나는 C++로 PS를 하는데, 참고했던 책도 그렇게 최신 책은 아니고 컴파일 환경도 C++14였어서 이런저런 화려한 문법들을 쓰지 않고 PS를 진행했다. 그런데, 다른 사람들의 코드를 읽어 보니 각종 편리한 문법들을 많이 사용하고 있는 것 같아서 나름 사람들 어깨 너머로 배운 내용들이나 편법(?)등을 정리해 놓고자 한다.
다른 사람들의 PS 코드야 예전부터 가끔 읽었지만, 그때는 딱히 내가 저 사람들이 쓰는 문법을 따라 할 필요가 없었다고 느꼈다. 그런데 언젠가부터 이런 마인드를 바꾸기로 결심했는데, 바로 취업을 위한 코딩 테스트를 준비할 때였다. 평소에 PS를 할 때에는 한 문제에 수십 분부터 몇 시간, 며칠을 매달리든 내 자유였지만 코딩 테스트는 그 시간이 정해져 있기 때문에 효율적인 알고리즘을 빠르게 찾아내는 것 이외에도 빠르게 코딩할 수 있는 능력이 나름대로 필요하다는 것을 알게 되었다. 그때부터, 하나씩 내가 사용하는 문법도 바꿔 보기로 했었다.
1. vector 사이즈 바꾸기
대부분의 문제에서 입력을 받을 때 sequential한 입력이 주어진다. 이는 배열 또는 벡터에 저장하게 되는데, 배열에 저장하는 경우는 그 사이즈를 능동적으로 바꿀 수 없어(PS에서 배열을 통한 동적 할당을 하기에는 코딩이 귀찮으니) 벡터를 주로 사용했다.
입력의 크기 N을 받으면, 이후의 입력을 벡터에 넣게 된다. 지금까지 생각해 왔던 방법은 두 가지였다.
- temp와 같이 새로운 변수를 할당해 temp에 입력을 받은 후, vector.push_back(temp)를 실행한다.
- vector를 N 사이즈로 초기화해서 for 문을 돌 때 vector[i]에 입력을 받는다.
그러나, 아래와 같은 방법 역시 존재한다.
int N;
vector<int> v;
cin >> N;
v.resize(N);
for (int i = 0; i < N; ++i) {
cin >> v[i];
}
그것은 바로 resize를 통해 벡터의 크기만을 바꾸는 것. N의 길이만큼 입력이 문제없이 들어온다는 가정 하에, 해당 코드 역시 아무런 문제 없이 돌아간다.
첫 번째 방법처럼 새로운 변수를 할당할 필요도, 두 번째 방법처럼 번거롭게 생성자를 타이핑할 필요도 없다.
2. 초기화 리스트의 사용
문제를 풀다 보면 직접 클래스를 생성해야 할 필요가 있다. DisjointSet같은 경우 수도 없이 만들어 본 클래스인데, 문제를 풀 때는 미리 만들어 놓은 클래스를 복사해서 붙여 넣어도 상관 없겠지만 까먹지 않기 위해 항상 0부터 새로 만든다. 이때, 클래스의 생성자에서 단순히 멤버 변수에 값을 할당해 주기만 하는 경우에 초기화 리스트의 사용을 고려할 수 있다.
class DisjointSet {
public:
vector<int> parent, rank;
DisjointSet(int n) : parent(vector<int>(n)), rank(vector<int>(n)) {
for (int i = 0; i < n; ++i)
parent[i] = i;
}
};
이때 vector와 같은 경우 resize할 수 없기 때문에 생성자를 넣는다. 또한, 초기화 리스트로 초기화한 후 생성자 내부 로직으로 추가적으로 초기화해 줄 수 있다. 그냥 알아 두면 좋은 정도이다.
3. make_pair()를 {}로 대체
이건 그냥 유용한 문법이라기보단 내가 몰라서 항상 노가다 식으로 타이핑했던 것 같은데, pair를 초기화하거나 return할 때 굳이 make_pair()를 사용하지 않아도 된다. 특히 Dijkstra의 알고리즘을 사용할 경우 priority_queue<pair<int, int> >와 같이 사용하는 경우가 많은데, 이 경우 일일이 make_pair()를 하지 않고 {}로 대체해도 된다.
priority_queue<pair<int, int> > pq;
pq.push({0, 0});
4. auto의 사용
auto는 말 그대로 컴파일 단계에서 타입 추론을 할 수 있는 경우 컴파일러가 자동으로 타입을 지정하는 것을 말한다. 일반적으로 복잡한 자료형 등을 직접 선언하지 않아도 된다는 장점이 있다.
예를 들어 map<string, int>::iterator 등을 직접 작성하지 않아도 되는 경우가 있다.
map<string, int> m;
map["hi"] = 111;
map["bye"] = 123;
for (auto iter = m.begin(); iter != m.end(); ++iter) {
// do something
}
m.begin()이 m의 타입인 map<string, int>의 iterator 타입을 반환하는 것은 자명한 사실이기 때문에 위와 같이 선언해도 아무런 문제가 없다.
5. 구조적 바인딩
C++17에 도입된 문법으로, js의 구조 분해 할당과 비슷하게 생각할 수 있다. VSCode에서 컴파일 환경이 C++14였는데도 C++17의 문법이라는 warning만 띄워 주고 제대로 돌아가던데 이건 왜였을까?
이 방법은 특히 pair나 tuple에서 값을 뽑아내는 데에 유용하다. 특히, tuple은 이 문법이 없으면 값을 뽑아내는 방법이 귀찮았었다. 내가 tuple<int, int, int>를 사용하는 대신에 항상 pair<int, pair<int, int> >를 사용했던 이유기도 했다.
tuple<int, string, double> t(1, "hi", 2.0);
int _a = get<0>(t);
string _b = get<1>(t);
auto _c = get<2>(t); // auto를 써도 된다
int _d;
string _e;
double _f;
tie(_d, _e, _f) = t; // std::tie를 사용해도 된다
상당히(?) 귀찮았던 tuple 값 뽑아 내기
방법은 간단하다. 형식은 auto로 지정하고, 빼낼 변수명을 [] 내부에 리스트로 집어 넣으면 된다.
tuple<int, string, double> t(1, "hi", 2.0);
auto [_a, _b, _c] = t;
첫 번째 방법처럼 get<T>()을 사용할 필요도, 두 번째 방법처럼 선언과 초기화를 따로따로 해 줄 필요도 없다. 이 방법은 pair에도 적용이 가능하다. 3의 Dijkstra의 알고리즘 코드를 이 문법을 이용하여 나머지를 작성해 보자.
priority_queue<pair<int, int> > pq;
pq.push({0, 0});
// additional code here
while (!pq.empty()) {
auto [cost, here] = pq.top();
pq.pop();
// do something
}
원래라면 int cost = pq.top().first, here = pq.top().second와 같이 일일이 적어줘야 했을텐데, 번거로움이 줄어들었다.
6. 범위 기반 for 문
for 문이라고는 for(int i = 0; i < n; ++i)만 갈겼던 나에게 찾아온 문법이다. index를 사용할 필요도 없고, 데이터 리스트에서 자동적으로 새로운 변수에 할당해 준다. 예를 들어, search를 해야 할 때 adjacency list를 사용하는 경우에 적용할 수 있다.
for (auto here : adj) {
search(here);
}
위와 같이 콜론(:)을 사용하여 구분하는데, 앞에는 데이터가, 뒤에는 뽑을 데이터를 담고 있는 리스트가 들어간다. 물론, 예시와 같이 auto 역시 사용 가능하다.
위와 같은 방식에는 두 가지 장점이 있다.
- index로 접근해야 하는 번거로움을 줄일 수 있다.
- for 문 내부에서, 리스트에서 뽑아 낸 데이터의 사용이 잦거나 여러 index가 겹칠 경우 혼동을 방지하기 위해서 각각을 새로운 변수에 할당하는 경우가 많은데, 이를 자동으로 수행해 준다.
그러나, 단점이라면 역시 index 정보가 없다는 점이다. 리스트 내부의 데이터만 뽑아 오는 경우라면 괜찮겠지만, index 정보를 사용해야 하는 경우도 꽤 존재하기 때문에 index 기반 for 문과 그 용도를 적절히 구분할 필요가 있다.
또한, auto를 참조자 형식으로 선언한다면 해당 값을 변경하는 것으로 기존의 리스트의 원소에 영향을 주는 것도 가능하다.
vector<int> v({1, 2, 3});
for (auto e : v) // 1
v *= 2;
for (auto &e : v) // 2
v *= 2;
1)의 for 문이 끝났을 때 v의 값은 [1, 2, 3] 그대로이지만, 2)의 for 문이 끝났을 때 v의 값은 [2, 4, 6]이 된다.
다른 사람의 코드를 읽어 보고 '아, 이렇게 쓸 수도 있구나!' 정도로 생각해서 알게 된 내용이고, documentation을 직접 보거나 하지는 않았기 때문에 틀린 내용이 있을 수도 있습니다. 지적은 항상 환영합니다 :)
'공부 > PS' 카테고리의 다른 글
[BOJ 1949] 우수 마을 (Gold II) (0) | 2022.11.17 |
---|---|
백준 64일 스트릭 회고 (0) | 2022.11.03 |
최단 거리 알고리즘의 이해 (0) | 2022.09.22 |
KMP 알고리즘 (0) | 2022.02.13 |