[종만북] (1)문제 해결 시작하기 - CH1. 문제 해결 개관 , CH2. 코딩과 디버깅에 관하여
2장. 문제 해결 개관
이 장을 읽으며 알고리즘 문제를 풀이할 때 나의 문제점을 찾아 낼 수 있었다.
알고리즘은 결국 문제의 해결을 요구하는것인데 나는 코딩에 너무 급급해서 코딩과 문제해결을 한번에 해결하려고 하다 결국 두마리 토끼를 모두 놓치는 것이라는 생각이 들었다.
여기서 나오는 몇가지 안좋은 사례 중 디버깅을 하며 뭔가 잘못됐음을 깨닫고 처음으로 되돌아가는게 너무 나인것 같아서 급할수록 돌아가라는 문구가 더욱 와 닿았다.😓
🤔 문제 해결 과정
- 문제를 읽고 이해하기
- 문제에 대한 설명을 공격적으로 읽고 문제가 원하는 바를 완전히 이해한다.
- 사소한 제약 조건을 놓치지 않는다.
- 재정의와 추상화
- 문제를 자신이 다루기 쉬운 개념을 이용해 자신의 언어로 풀어 쓴다.
- 문제를 추상화(현실의 개념을 수학,전산적 개념으로 옮겨 표현)하여 다루기 쉽게 만든다.
- 계획 세우기
- 문제 해결을 위한 방식, 알고리즘, 자료구조를 결정한다.
- 계획 검증하기
- 3에서 설계한 알고리즘이 모든 경우에 요구 조건을 정확히 수행하는지 증명한다.
- 수행 시간과 메모리가 제한에 부합하는지 확인한다.
- 계획 수행하기(코딩)
- 회고하기
- 자신이 문제를 해결한 과정을 돌이켜보고 개선한다.
- 문제를 풀 때마다 코드와 함께 경험(해결법, 접근법, 결정적 깨달음)을 기록으로 남긴다.
- 한번에 맞추지 못했다면 오답의 원인도 반드시 적는다. (오답 노트)
- 다른 사람의 코드를 본다.
🌟 나의 경우에는 코딩 전에 손으로 문제의 해결 과정을 정리하고 검증하는 것이 필요할 듯 하다!
🤔 문제 해결 전략
-
직관과 체계적인 접근이 필요하다.
-
체계적 접근을 위한 질문
- 비슷한 문제를 풀어본 적이 있었나?
- 이를 위해 알고리즘의 원리를 이해하며 문제를 풀어야 함
- 단순한 방법에서 시작할 수 있을까?
- 문제를 과대평가해서 어렵게 푸는 실수를 예방
- 알고리즘 효율성의 기준선을 정해줌
- 가장 간단한 방법으로부터 점진적으로 사고를 발전시켜 해결법을 개선할 수 있음
- 내가 문제를 푸는 과정을 수식화 할 수 있나?
- 수식화를 통해 답을 찾거나 알고리즘의 고려점을 파악
- 손으로 문제를 푸는 습관을 통해 알고리즘에 간과한 점이 없는지 미리 확인
- 문제를 단순화 할 수 없나?
- 문제의 제약 조건 삭제, 저차원화(ex: 2차원→1차원) 등을 통해 문제를 단순화하여 통찰을 얻을 수 있음
- 그림으로 그려볼 수 있나?
- 수식으로 표현할 수 있나?
- 평문으로 쓰인 문제를 수식으로 표현
- 문제를 분해할 수 있나?
- 문제의 복잡한 조건을 단순한 여러 개의 조건으로 분해
- 뒤에서부터 생각해서 문제를 풀 수 있나?
- 예를 들어 사다리 게임
- 순서를 강제할 수 있나?
- 순서가 없는 문제에, 답에 관계없는 순서를 강제해 문제를 단순화함
- 특정 형태의 답 만을 고려할 수 있나? (정규화)
- 고려해야 할 답 중 형태가 다르지만 결과적으로 같은 것들을 그룹으로 묶고, 각 그룹의 대표들만을 고려
- 비슷한 문제를 풀어본 적이 있었나?
3장. 코딩과 디버깅에 관하여
위에서 코딩보다 문제 해결이 중요한 것 같다고 한지 얼마 되지 않았지만, 이 장에서는 코딩의 중요성에 대해 말하고 있다. 나의 경우에 이 부분은 경험으로 연습이 좀 되어있어서 문제 해결 부분이 더 중요했지만 읽다 보니 코딩도 중요하긴 하다.
코딩은 내가 푼 문제를 표현하는 수단이니 방법을 정확히 익혀서 내가 표현하고자하는것을 정확히 표현해야한다.
🤔 코드 스타일을 일관되게 결정하는 게 좋겠다!
🔤 좋은 코드를 짜기 위한 원칙
-
간결한 코드 작성하기
: 짧고 간결한 코드로 오류 발생 가능성 줄임
-
적극적으로 코드 재사용
: 여러 번 쓰이는 함수는 모듈화 해서 재사용
-
표준 라이브러리 공부
: 간결하고 가독성 높은 코드를 만들기 위해 검증된 표준 라이브러리 사용
-
항상 같은 형태로 프로그램 작성
: 자주 작성하는 알고리즘, 코드 등은 한번 검증된 코드를 만들면 이것만 꾸준히 사용하여 도구가 아닌 문제에 집중
- 일관적이고 명료한 명명법 사용
-
모든 자료를 정규화 해서 저장
: 예를 들어 분수는 모두 기약 분수로 정규화, 각도는 0~359도 사이로 정규화
-
코드와 데이터 분리
: 오타를 주의
🔤 자주 하는 실수
- 산술 오버플로
- 배열 범위 밖 원소에 접근
- 일관되지 않은 범위 표현 방식 사용
- 일반적으로 [low,high)로 범위 사용
- Off-by-one오류
- 계산의 흐름은 맞지만 하나가 모자라거나 많아서 틀리는 코드의 오류
- 컴파일러가 잡아주지 못하는 상수 오타
- 스택 오버플로
- 대개 재귀 호출의 깊이가 깊어질 때 발생
- 다차원 배열 인덱스 순서 바꿔쓰기
- 특정 배열에 접근하는 위치를 하나로 통일하는 것을 권장
- 잘못된 비교 함수 작성
- 사용하는 언어의 연산자의 성질을 익혀서 검토
- 예를 들어 c++의 경우 ‘<’연산자는 아래 성질을 가짐
- a<a는 항상 거짓 (비반사성)
- aa는 거짓 (비대칭성)
- a<b, b<c 참이면, a<c도 참 (전이성)
- a<b, b<a에 모두 거짓이면 a=b (상등 관계의 전이성)
- 정렬 시 위 연산자 성질에 모순되는 정렬 기준이 생성되면 정렬 실패
- 최소, 최대 예외 잘못 다루기
- 가장 작은 입력과 가장 큰 입력에 대한 동작을 고려하기
- 연산자 우선순위 잘못 쓰기
- 너무 느린 입출력 방식 선택
- 변수 초기화 문제
- 전역변수를 초기화 하지 않고 사용시 두번째 케이스 부터는 답이 틀릴 수 있음
- 같은 입력 예제를 두 번 반복해서 입력하여 초기화 여부 확인 가능
🔤 디버깅과 테스팅
🐞 디버깅
- 디버거 사용은 지양
- 대안이 되는 검증 단계
- 작은 입력에 대해 제대로 실행되나 확인
- 단정문(조건이 거짓일때 오류를 내고 프로그램 종료시키는 함수)을 통해 조건 확인
- 프로그램 계산 중간 결과 출력
- 런타임 오류의 경우 디버거를 사용해 스택기록 확인
✔️ 테스트
- 제출 전 가능한 많은 예제 입력으로 테스트
- 스캐폴딩
- 답안을 검증하기 위해 짜는 임시코드
- 코드의 정당성 확인 , 반례 탐색에 유용
- 더 느리고 간단한 알고리즘으로 스캐폴딩을 만들고 나오는 답과 비교 하며 테스트 가능
🔤 변수 범위의 이해(산술 오버플로)
- 계산된 값이 자료형의 표현가능 범위를 벗어난 경우를 의미
- 대부분 사전에 경고를 해주지 않으므로 주의
🤯 산술 오버플로가 일어나는 경우
- 너무 큰 결과
- 큰 정수를 다룰 때는 항상 변수의 형태에 주의
- 너무 큰 중간값
- 계산 중간에 큰값을 일시적으로 계산 해야하는 경우 계산 결과가 이상해 짐
- 너무 큰 무한대 값
- 자료형이 처리 할 수 있는 가장 큰값을 무한대 값으로 쓰는 실수
- 무한대 값이 서로 더해지거나 곱해지는 경우를 고려하여 적당한 값으로 무한대 값을 설정해야함.
🤯 오버플로를 피해가는 법
- 더 큰 자료형 사용하기
- 연산의 순서를 바꾸기
- 계산 방법을 바꾸기 ex) 이항계수의 경우 팩토리얼을 사용한 계산 → 점화식을 사용한 계산
🔤 실수 자료형의 이해
자료형의 bit수가 제한되어 있으므로 근사값을 사용해 실수를 표현하게 된다.
따라서 실수 자료형을 사용할때는 근사로 인한 오차를 고려해야한다.
- IEEE 754 표준
- 대부분의 컴퓨터,컴파일러에서 사용 되는 실수 표기방식
- 특징
- 이진수로 실수표기
- 부동 소수점 표기법 : 소수점을 움직일수 있음
- 무한대, 비정규 수,NaN 등 특수값이 존재
-
실수의 이진법 표기
1011.101
→ 정수부 1011 : $2^3+2^1+2^0$
→ 소수부 001 : $(1/2)^1+(1/2)^3$
- 부동 소수점 표기
- 정수부와 소수부에 비트 배분을 자유롭게 하기 위해 소수점을 옮길 수 있도록 함
- 이게 소수점이 둥둥 떠다니는것 같다고 ‘부동’이라는 이름 붙음
-
32bit 실수 표기의 경우 아래와 같은 구조로 이루어짐
구분 MSB(most significant bit) 지수부 가수부 bit 1 8 32 설명 부호 비트 소수점을 몇 칸 옮겼나 소수점을 옮김 실수의 최상위 X비트 -
자료형 별 부동소수점 표기
자료형 MSB(bit) 지수부(bit) 실수부(bit) 지수 범위 유효 자릿수(십진수) 32bit 실수형 1 8 23 -2^7+2 ~ 2^7-1 6 64bit 실수형 1 11 52 -2^10+2 ~ 2^10-1 15 80bit 실수형 1 15 64 -2^14+2 ~ 2^14-1 18 - 웬만하면 그냥 64비트 이상 실수형 사용을 권장
-
실수 비교하기
위와 같이 컴퓨터는 실수를 근사하여 표현하므로 실수를 비교할때는 근사에서 오는 오차를 고려해야함.
- 비교할 실수의 크기들에 비례한 오차한도 설정
- 단위가 있을경우 사용
-
같다고 판단할 큰 값 두개를 비교할 경우 고려
: 가능한 오차의 최대치를 가늠하여 오차 한도의 하한 설정
-
다르게 판단할 작은 값 두개를 비교할 경우 고려
: 다르다고 처리되야할 두 값이 오차때문에 가까워지는 경우
: 가능한 오차의 하한를 가늠하여 오차 한도의 상한 설정
- 상대 오차 이용
- 단위가 없거나 일반적인 경우에 사용
- 비교하려는 숫자의 크기에 비례하여 오차를 정하는 방식
-
\[상대오차(a,b)=\frac{|a-b|}{max(|a|,|b|)}\]
//a,b의 오차가 더 큰수의 0.000001%이하면 true 반환 boolean relativeEqual(double a, double b){ return Math.abs(a-b) <= 1e-8*Math.max(Math.abs(a),Math.abs(b)); }
-
0에 매우 가까운 작은 수를 비교하려 할때는 주의해야함 → 작은수가 0으로 표현되어 상대오차가 1이 될 수 있음 → 따라서 절대오차와 상대오차를 적절히 혼용
boolean doubleEqual(double a, double b){ double diff=Math.abs(a-b); // 절대오차 계산 if(diff<1e-10) return true; // 절대 오차가 허용범위 내라면 true반환 return diff<=1e*max(Math.abs(a),Math.abs(b)); // 이외의 경우 상대오차 사용 }
- 대소비교
- 오차로 인한 이상한 결과를 피하려면 두값이 같은경우(두 값이 아주 가까운경우)를 먼저 처리해줘야함
- 정확한 사칙연산
- 가수부 안에 들어가는 정수들, 분모가 2의 거듭제곱인 유리수 등 정확히 표현 가능한 범위의 숫자 사용
- 추가적인 자료구조 도입( ex) java의 BifDecimal )
- 코드의 수치적 안정성 파악
-
코드의 수치적 안정성(numerically stable)
: 프로그램 실행 과정에서 발생하는 오차가 더 커지지 않는것
: 수치적으로 안정된 프로그램에서는 중간의 작은 연산 오차가 큰 오차를 야기시키지 않음
-
실수연산이 필요하다면 수치적으로 안정된 알고리즘을 짜야함
-
- 실수 연산 하지 않기
- 실수 연산을 제대로하는것은 어려움
- 실수 연산을 아예 하지 않는것이 가장 좋은 방법!
- 적절한 변형을 통해 실수 연산을 피하자!
- 비교할 실수의 크기들에 비례한 오차한도 설정
댓글남기기