계속 수정됩니다.
문제 해결 능력을 기르기 위한 방법 중 효과적인 것이 프로그래밍 대회 참가
프로그래밍 대회들은 주로 짧은 요구사항을 제시하고, 이에 맞는 프로그램을 시간 안에 빠르게 작성하는 능력을 겨루는 대회
문제 형식은 '어떤 값을 읽어들여 어떤 값을 계산하는 프로그램을 작성하시오'
ACM-ICPC 에서는 참고 자료로써 20여 장 출력물 지참 가능
1. 미리 준비(문제 풀 때 유용한 것 정리)
2. 간단한 형태의 문서화(역할, 실행 전·후 조건)
3. 클래스 형태 준비
4. 적절한 글꼴
사소한 제약 조건을 잘못 이해해서 풀 수 없게 되는 문제들이 흔하므로 문제가 원하는 바를 완전히 이해하자.
문제를 자신의 언어로 풀어 쓰자.
복잡한 현실 세계의 개념을 본질만 남겨두고 축약하여 다루기 쉽게 표현하자.
어떤 방식으로 해결할지, 사용할 알고리즘과 자료구조를 선택하자.
설계한 알고리즘이 모든 경우에 요구 조건을 정확히 수행하는지 증명하고, 수행에 걸리는 시간, 사용하는 메모리가 문제의 제한 내에 들어가는지 확인하자.
설계한 알고리즘을 정확하고 효율적으로 구현하자.
문제를 해결한 과정을 돌이켜 보고 개선하자.
더 효율적인 알고리즘을 찾거나 간결한 코드를 작성하고, 같은 알고리즘을 유도할 수 있는 더 직관적인 방법을 찾자.
문제를 풀지 못할 때는 일정 시간이 지나도록 고민해도 답을 찾지 못할 때는 다른 사람의 소스 코드나 풀이를 참조한다는 원칙을 세우고 이를 지키자.
단 참조를 했을 경우 반드시 복기를 동반하자.
해당 문제를 해결하는 알고리즘이 대략적으로 어떤 형태를 가질지를 짐작할 수 있게 하자.
알고리즘 유형을 분류하는 방법을 익히고 그 동작 과정과 원리를 완전히 이해하자.
어느 경우에 사용될 수 있는지 체계적으로 공부해야 한다.
시간, 공간 제약을 생각하지 않고 가장 단순한 알고리즘을 만들어 보자.
좀더 효율적인 자료 구조를 사용하거나, 계산 과정에서 같은 정보를 두 번 중복으로 계산하지 않는 등의 최적화를 적용해서 충분히 빨라질 때까지 알고리즘을 개선할 수 있다.
점진적인 개선을 통해 문제를 풀 수 없더라도 단순한 방법은 알고리즘 효율성의 기준선을 정해준다.
손으로 여러 간단한 입력을 직접 해결해보자. 이 과정에서 알고리즘이 어떤 점을 고려해야 하는지 알게 된다. 어떻게 문제를 풀어야 할지 감을 잡기에도 유용하다.
주어진 문제의 좀더 쉬운 변형판을 먼저 풀어보자.
문제의 제약 조건 제거, 계산해야 하는 변수의 수 감소, 다차원의 문제를 1차원으로 감소하는 방식이 있다.
두 개의 정수 쌍들을 다루는 문제가 있다면, 이 정수 쌍을 2차원 평면 상의 좌표로 그려 볼 수 있고, 직선 상의 구간들로 그려 볼 수 있다.
평문으로 쓰여 있는 문제를 수식으로 표현하자. 수식을 전개하거나 축약하는 등의 순수한 수학적 조작이 문제를 해결하는 데 도움을 줄 수 있다.
복잡한 조건을 더 단순한 형태를 갖는 조건의 집합으로 분해하자.
문제에 내재된 순서를 바꿔보자.
A에서 B로 가는 것보다 B에서 A로 가는 것이 빠를 수 있다. (사다리 게임)
순서가 없는 문제에 순서를 강제해서 문제를 풀어보자.
경우의 수를 셀 때 유용하다. 특정 조건을 만족하는 답들의 수를 세는 문제에서 한 가지 답을 두 가지 이상의 방법으로 만들 수 있을 때 답이 중복해서 세어지는데 할 수 있는 일들의 순서를 강제하는 것이 좋은 해법이 된다. (오름차순으로만 본다던지?)
정규화(canonicalization)란 우리가 고려해야 할 답들 중 형태가 다르지만 결과적으로는 똑같은 것들을 그룹으로 묶은 뒤, 각 그룹의 대표들만을 고려하는 방법이다.
정규화 기법을 사용하기 위해서는 어떤 답이 주어지더라도 이것을 특정 형태의 답으로 바꿀 수 잇는 변형 과정을 찾아야 한다.
간결하고 일관되게 코드를 작성하여 읽기 쉽고 디버깅이 쉽게 하자.
#define FOR(i,n) for(int i=0; i<(n); ++i)
FOR(i, array.size())
FOR(j, i)
// 이런 식으로 하면 증감문 오타같은 경우를 대비할 수 있음
실수를 피해 가는 올바른 방법은 foreach 구문 이용! C++11 부터 구간 기반 for문 지원
코드를 모듈화하자. 같은 코드가 세 번 이상 등장한다면 함수로 분리해 재사용한다는 기본 원칙을 만들면 좋다.
한 함수가 두 가지 이상의 일을 해서는 안 된다는 것이 이상적인 코드
모든 코드를 직접 작성하는 것은 시간 낭비. 팀원의 이해를 돕기 위해서라도 표준 라이브러리의 사용은 필수.
문자열, 동적 배열, 스택, 큐, 리스트, 딕셔너리 등의 자료 구조, 정렬 등의 표준적인 알고리즘 구현 사용법을 알아두자.
같은 코드를 작성하는 더 좋은 방법에 대해 꾸준히 고민할 필요는 있지만,
자주 작성하는 알고리즘이나 코드 등에 대해서는 한 번 검증된 코드를 작성하고 이것만을 꾸준히 사용할 필요가 있다. 그래야 도구가 아니라 문제에 집중할 수 있다!
사용하는 언어의 표준 라이브러리에서 사용하는 명명 규악(naming convention)을 익히자.
모호하지 않은 변수명과 함수명을 사용하자.
같은 자료를 두 가지 형태로 저장하지 않는 것이 좋다.
유리수를 예로 들면 항상 약분해서 기약 분수로 표현하는 것이 좋다.
자료를 표현하는 클래스의 생성장에서 정규화를 수행하거나, 외부에서 자료를 입력받자마자 정규화를 수행하는 것이 이상적이다.
날짜를 영문 이름으로 출력할 때
if(month == 1) return "January";
...
이와 같은 형식으로 작성하면 안 좋다.
코드의 논리와 상관 없는 데이터는 가능한 한 분리하는 것이 좋다.
영문 이름을 별도의 배열에 저장하는 방식이 있다.
변수의 표현 범위를 벗어나는 값을 사용하는 산술 오버플로.
배열 크기를 정할 때 계산을 신중히 하자.
인덱스를 0에서 시작하는지 1에서 시작하는지 주의하자.
자연수 범위 [2, 3, 4, …, 12] 를 표현하고 싶을 때
닫힌 구간: [2, 12]
열린 구간: (1, 13)
반 열린 구간: [2, 13)
C++ STL에서는 반복자(Iterator)로 범위를 표현할 때 첫 원소를 가리키는 반복자와 마지막 원소 다음 위치를 가리키는 반복자를 사용한다. begin() 과 end()
장점
한 가지 방법으로만 범위를 표현해야 혼란을 초래하지 않음
반복문에서 < 혹은 > 연산자와 ≤ 와 ≥ 연산자를 혼용할 경우 흔하게 발생한다.
최소 입력이 주여졌을 때 이 코드가 어떻게 동작할지를 되새겨 보면서 프로그램을 짜면 좋다.
콜 스택이 오버플로해서 프로그램이 강제종료 될 수 있다.
재귀 호출이 깊이가 너무 깊어지면 오는데, 스택 허용량에 대해 알아둘 필요가 있다.
스택 메모리를 절약하기 위해 자동으로 힙에 메모리를 할당하는 STL 컨테이너를 사용하거나 전역 변수를 사용하곤 한다.
동적 계획법을 위한 메모이제이션 패턴을 사용할 때 잦다.
가능한 한 특정 배열에 접근하는 위치를 하나로 통일하는 것이 좋다.
C++의 참조 변수를 사용해 해결하자.
정렬할 때 비교 함수를 잘못 작성해서 원하는 순서를 얻지 못할 수 있다.
< 연산자의 성질
가장 작은 입력과 가장 큰 입력에 대해 제대로 동작할지를 생각해 보자.
if(b & 1 == 0)
// 위 코드는 다음과 같은 의미
if(b & (1 == 0))
연산자의 우선순위를 잘 기억하거나, 헷갈릴 경우 괄호로 적절히 감싸자.
gets() 를 이용해 모든 입력을 문자열 하나로 읽어들여 파싱할 수 있고, cin 등의 고수준 입력 방식을 사용할 수 있지만 cin 은 속도가 느리다.
cin.sync_with_stdio(false) 를 사용하자. (cstdio 의 표준 입출력 함수들과의 동기화 해제)
초기화를 하지 않고 변수를 그대로 사용하면 두 번째 입력부터는 답이 잘못 나올 수 있다.
예제 입력 파일을 두 번 반복해 쓰는 것이 방법이다.
변수를 적절히 초기화하도록 신경 써서 코딩하는 것이 가장 좋은 방법이다.
디버거를 사용하는 대신 디버깅하는 팁
주어진 예제 입력을 약간 바꿔서 넣어 보거나, 있을 수 있는 가장 작은 입력과 가장 큰 입력을 만들어서 넣어 보고 시간 안에 실행되는지, 답은 잘 나오는지 테스트하자.
스캐폴딩: 다른 코드를 개발할 때 뼈대를 잡기 위해 임시로 사용하는 코드
스캐폴딩으로 코드의 정당성을 확인하거나 반례를 찾는 데 유용하게 쓰자.
임의의 작은 입력을 자동으로 생성해 프로그램을 돌려 보고, 그 답안을 검증하는 프로그램을 짜면 쉽게 테스트할 수 있다.
어떤 식의 계산 값이 반환되는 자료형의 표현 가능한 범위를 벗어나는 경우
일어나는 이유
1. 대부분의 프로그래밍 언어들은 사칙연산 과정에서 오버플로가 나더라도 경고를 하지 않는다.
2. 프로그램의 정당성을 검증할 때 프로그램 논리의 정확성에만 집중하다 보면 산술 오버플로를 고려하지 못할 수 있음.
큰 정수를 다룰 때는 항상 변수의 형태에 주의하는 습관을 가지자.
64비트의 정수를 이용하면서 중간 계산 값을 32비트 정수로 전달할 수 있으니 주의하자.
프로그램의 출력 값의 범위는 작지만 중간 과정에서 큰 값을 일시적으로 계산해야 하는 경우.
최단 경로를 구할 때 길이 없는 경우는 굉장히 큰 값을 더해줘서 min 함수에서 걸러지게 할 수 있다.
무한대 값을 선택할 때는 이 무한대 값들이 서로 더해지거나 곱해지는 경우가 없는지 잘 살펴보고 오버플로가 나지 않을 크기의 값을 선택하는 것이 좋다.
987,654,321 은 2의 30제곱에 가까운 매우 큰 값이면서 서로 더해지더라도 32비트 부호 있는 정수에 오버플로를 내지 않고, 오타가 났는지 확인하기 용이하다.
가장 간단한 방법은 더 큰 자료형 쓰는 것.
이항 연산자에서 만약 피연산자의 자료형이 다르거나 자료형의 범위가 너무 작은 경우 컴파일러들은 대개 이들을 같은 자료형으로 변환해서 계산한다. 이를 프로모션이라 한다.
C++ 의 프로모션 규칙
1. 한쪽은 정수형이고 한쪽은 실수형이면 정수형이 실수형으로 변환
2. 양쪽 다 정수형이거나 양쪽 다 실수형이면 보다 넓은 범위를 갖는 자료형으로 변환
3. 양쪽 다 int형보다 작은 정수형이면 양쪽 다 int형으로 변환
4. 부호 없는 정수형과 부호 있는 정수형이 섞여 있으면 부호 없는 정수형으로 변환
C++ STL 에서 모든 컨테이너의 size() 는 부호 없는 정수형을 반환한다.
이 때 크기가 0인데 -1 을 한다면 가장 큰 수가 되어버린다.
1/x * x = 1인 x의 수를 세는 코드를 작성하면 원하는 결과를 얻을 수 없음
컴퓨터의 모든 실수 변수는 정확도가 제한된 근사 값을 저장한다.
소수점 밑 i번째 자리의 크기는 1/(2^i)
최상위 비트 1은 생략하므로 가수부가 52비트면 총 53비트 표현할 수 있다.
64비트 실수형: 부호 비트(1), 지수 비트(11), 가수 비트(52), 유효자릿수(15(10))
비교할 때 항상 두 값이 같은 경우, 즉 두 값이 아주 가까운 경우를 먼저 확인하고 처리해주어야 한다.
IEEE 표준에 의해 실수 변순들은, 정확하게 표현할 수 있는 값은 항상 정확하게 저장하도록 구현되어 있다.
64비트 실수형의 가수부는 52비트이므로 그 범위까지는 정확하게 표현할 수 있다.
추가적인 자료 구조를 도입해서 정확한 사칙연산을 구현할 수도 있다.
프로그램의 실행 과정에서 발생하는 오차가 더 커지지 않으면 수치적으로 안정적이라고 한다.
『Clean Code』(인사이트): 변수, 함수의 명명법, 함수의 구성, 주석을 쓰는 법까지 깨끗하고 잘 구성된 코드의 여러 원칙과 적용 예를 보여준다.
실수 비교에 관한 심도있는 내용