C++ 코딩 테스트 준비: 백준·프로그래머스 유형별 STL 활용과 입출력 최적화

이 글의 핵심

C++로 코딩 테스트를 볼 때는 알고리즘보다 STL을 제대로 못 써서 시간 초과나 오답이 나는 경우가 많습니다. map과 unordered_map, vector와 set 중 무엇을 고를지, sort와 lower_bound 같은 algorithm 함수를 어떻게 조합할지를 문제 유형과 연결해 설명하고, 엣지 케이스 체크와 두 플랫폼의 입출력 방식 차이도 짚습니다.

코딩 테스트는 알고리즘과 자료구조 문제를 제한 시간 안에 코드로 푸는 시험입니다. 온라인 저지 형태든 면접관 앞에서 화이트보드나 온라인 IDE로 푸는 형태든, 자료구조와 알고리즘 기본기와 함께 C++에서는 STL을 얼마나 정확히 쓰는지가 평가됩니다. 이 글은 자주 나오는 문제 유형별로 필요한 STL과 코드 패턴을 정리하고, 입력 크기로 허용 복잡도를 가늠하는 법, 입출력 최적화, 백준과 프로그래머스의 차이를 다룹니다.


자주 나오는 알고리즘 유형 7가지

정렬과 탐색

배열을 정렬한 뒤 특정 값을 찾거나, K번째로 큰 원소를 구하거나, 두 배열의 교집합을 구하는 문제가 대표적입니다. std::sort, std::binary_search, std::lower_bound, std::upper_bound, std::nth_element를 주로 씁니다.

정렬 자체는 O(N log N)이지만, 정렬해 두면 이진 탐색(O(log N))이나 투 포인터(O(N))를 쓸 수 있습니다. 이중 반복문으로 O(N²)에 풀 문제를 정렬과 이진 탐색으로 O(N log N)에 해결하는 것이 이 유형의 핵심입니다.

lower_bound(x)는 x 이상인 첫 위치를, upper_bound(x)는 x보다 큰 첫 위치를 돌려줍니다. 따라서 upper_bound - lower_bound는 정렬된 범위에서 x의 개수입니다.

// g++ -std=c++17 -o binsearch_demo binsearch_demo.cpp && ./binsearch_demo
#include <vector>
#include <algorithm>
#include <iostream>

int main() {
    std::vector<int> v = {3, 1, 4, 1, 5};
    std::sort(v.begin(), v.end());  // {1, 1, 3, 4, 5}
    bool found = std::binary_search(v.begin(), v.end(), 3);
    auto it = std::lower_bound(v.begin(), v.end(), 3);
    std::cout << found << " " << (it - v.begin()) << "\n";  // 1 2
    return 0;
}

해시맵 (빈도수, 중복 체크)

배열에서 중복 원소 찾기, 두 문자열이 애너그램인지 확인하기, 누적합과 해시맵으로 합이 K인 구간 개수 세기 같은 문제에 std::unordered_map과 std::unordered_set을 씁니다. 해시맵은 삽입, 삭제, 탐색이 평균 O(1)이므로, 이중 반복문으로 O(N²)에 풀어야 할 문제를 O(N)으로 줄이는 경우가 많습니다.

“배열에서 합이 K인 두 원소 찾기”를 예로 들면, 모든 쌍을 확인하는 방법은 O(N²)입니다.

for (int i = 0; i < n; ++i) {
    for (int j = i + 1; j < n; ++j) {
        if (arr[i] + arr[j] == K) { /* 찾음 */ }
    }
}

지금까지 본 값을 집합에 넣어 두면, 각 원소마다 짝(K - x)이 이미 나왔는지 평균 O(1)에 확인할 수 있어 전체가 O(N)이 됩니다.

std::unordered_set<int> seen;
for (int x : arr) {
    if (seen.count(K - x)) { /* 찾음 */ }
    seen.insert(x);
}

빈도수 세기는 operator[]가 없는 키를 0으로 초기화해 넣어 주는 성질을 이용합니다.

std::unordered_map<int, int> freq;
for (int x : arr) {
    freq[x]++;
}

// 가장 빈도 높은 원소
int maxFreq = 0, result = 0;
for (auto& [num, cnt] : freq) {
    if (cnt > maxFreq) {
        maxFreq = cnt;
        result = num;
    }
}

주의할 점은 평균 O(1)이 최악 O(N)이라는 것입니다. 해시 충돌을 노린 입력이 들어오는 Codeforces 같은 대회에서는 unordered_map이 시간 초과를 내는 경우가 있고, 그럴 때는 map(O(log N))이나 정렬 후 처리로 바꿉니다. 또 freq[x]로 존재 여부를 확인하면 없는 키가 새로 삽입되므로, 확인만 할 때는 count나 find를 씁니다.

투 포인터 (Two Pointers)

정렬된 배열에서 합이 K인 두 원소 찾기, 합이 K 이하인 가장 긴 부분 배열 찾기, 회문 판별 같은 문제에서 왼쪽과 오른쪽 포인터를 움직이며 O(N)에 답을 찾습니다.

// 정렬된 배열에서 합이 target인 두 원소
std::vector<int> v = {1, 2, 3, 4, 5};
int target = 7;
int left = 0, right = (int)v.size() - 1;

while (left < right) {
    int sum = v[left] + v[right];
    if (sum == target) {
        std::cout << v[left] << ", " << v[right] << '\n';
        break;
    } else if (sum < target) {
        left++;   // 합을 키워야 하므로 작은 쪽을 오른쪽으로
    } else {
        right--;  // 합을 줄여야 하므로 큰 쪽을 왼쪽으로
    }
}

배열이 정렬되어 있기 때문에 합이 작으면 left를 늘리는 것 외에는 합을 키울 방법이 없고, 그래서 버린 쌍 중에 정답이 없다는 것이 보장됩니다. 정렬되지 않은 배열에 그대로 쓰면 답을 놓칩니다. v.size() - 1을 int로 캐스팅한 것은 빈 벡터에서 size_t 언더플로로 아주 큰 값이 되는 것을 막기 위해서입니다.

동적 계획법 (DP)

동적 계획법은 작은 부분 문제의 답을 저장해 두고 큰 문제를 풀 때 재사용하는 기법입니다. 피보나치 수열, 최장 증가 부분 수열(LIS), 배낭 문제가 대표적입니다.

순수 재귀로 fib(5)를 구하면 fib(4)와 fib(3)을 부르고, fib(4)가 다시 fib(3)을 불러 같은 값을 여러 번 계산합니다. 호출 수가 지수적으로 늘어 fib(50)은 약 400억 번의 함수 호출이 필요하지만, 계산한 값을 저장하면 서로 다른 부분 문제 50여 개만 계산하면 됩니다.

재귀에 캐시를 붙이는 Top-Down(메모이제이션) 방식은 필요한 값만 계산하고, 반복문으로 작은 문제부터 채우는 Bottom-Up(타뷸레이션) 방식은 함수 호출 오버헤드와 재귀 깊이 문제가 없습니다.

// Top-Down: fib(92)까지 long long에 들어갑니다 (fib(93)은 범위 초과)
std::vector<long long> dp(93, -1);

long long fib(int n) {
    if (n <= 1) return n;
    if (dp[n] != -1) return dp[n];  // 이미 계산됨
    return dp[n] = fib(n - 1) + fib(n - 2);
}
// Bottom-Up
std::vector<long long> dp(93);
dp[0] = 0; dp[1] = 1;
for (int i = 2; i < 93; ++i) {
    dp[i] = dp[i - 1] + dp[i - 2];
}

배열 크기를 넉넉하게 100으로 잡고 끝까지 채우면 dp[93]부터 부호 있는 정수 오버플로(미정의 동작)가 일어납니다. 문제에서 “1,000,000,007로 나눈 나머지”를 요구하는 이유가 바로 이것이므로, 값이 커지는 DP는 범위부터 확인합니다.

그래프 탐색 (BFS, DFS)

그래프는 정점과 간선으로 이루어진 자료구조입니다. 미로의 최단 경로(BFS), 연결 요소 개수(DFS/BFS), 사이클 탐지, 위상 정렬이 자주 나옵니다. 인접 리스트는 std::vector<std::vector<int>>로, BFS는 std::queue로, DFS는 재귀나 std::stack으로 구현합니다.

std::vector<std::vector<int>> graph(n);
std::vector<bool> visited(n, false);
std::queue<int> q;

q.push(start);
visited[start] = true;

while (!q.empty()) {
    int node = q.front();
    q.pop();

    for (int neighbor : graph[node]) {
        if (!visited[neighbor]) {
            visited[neighbor] = true;  // 큐에 넣을 때 방문 표시
            q.push(neighbor);
        }
    }
}

방문 표시를 큐에서 꺼낼 때가 아니라 넣을 때 해야 같은 정점이 큐에 여러 번 들어가지 않습니다. 이 순서를 바꾸면 결과는 맞아도 큐가 커져 메모리 초과나 시간 초과가 나는 경우가 많습니다. 간선 가중치가 모두 같을 때만 BFS가 최단 경로를 보장하며, 가중치가 다르면 다익스트라를 써야 합니다.

그리디 (Greedy)

매 순간 가장 좋아 보이는 선택을 해도 전체 최적해가 보장되는 문제에 씁니다. 회의실 배정(활동 선택 문제), 화폐 단위가 서로 배수 관계인 동전 거스름돈, 최소 신장 트리(Kruskal, Prim)가 대표적입니다. 그리디의 어려운 점은 코드가 아니라 “이 선택이 왜 항상 최적인가”를 확인하는 것이며, 동전 단위가 {1, 3, 4}이고 6을 만들 때처럼 그리디(4+1+1)가 최적(3+3)과 다른 경우가 있으면 DP로 풀어야 합니다.

struct Meeting {
    int start, end;
};

std::vector<Meeting> meetings = {{1, 3}, {2, 4}, {3, 5}};

// 끝나는 시간 기준 정렬 (같으면 시작 시간 기준)
std::sort(meetings.begin(), meetings.end(), [](const Meeting& a, const Meeting& b) {
    if (a.end != b.end) return a.end < b.end;
    return a.start < b.start;
});

int count = 0, lastEnd = 0;
for (auto& m : meetings) {
    if (m.start >= lastEnd) {
        count++;
        lastEnd = m.end;
    }
}

끝나는 시간이 같을 때 시작 시간으로 정렬하지 않으면, (2, 2)처럼 시작과 끝이 같은 회의가 섞인 입력에서 답이 달라집니다. 백준 1931번이 이 경우를 시험합니다.

백트래킹 (Backtracking)

N-Queen, 순열과 조합 생성, 스도쿠처럼 모든 경우를 재귀로 시도하되 조건에 맞지 않으면 즉시 되돌아오는 문제입니다.

void permute(std::vector<int>& nums, size_t start) {
    if (start == nums.size()) {
        for (int x : nums) std::cout << x << ' ';
        std::cout << '\n';
        return;
    }

    for (size_t i = start; i < nums.size(); ++i) {
        std::swap(nums[start], nums[i]);
        permute(nums, start + 1);
        std::swap(nums[start], nums[i]);  // 원래 상태로 되돌림
    }
}

swap 방식은 사전순으로 출력되지 않습니다. 사전순이 필요하거나 중복 원소가 있으면 std::next_permutation을 씁니다. 처음에 반드시 정렬해야 모든 순열을 돌며, 중복 원소가 있어도 같은 순열을 한 번씩만 만듭니다.

std::vector<int> v = {1, 2, 3};
std::sort(v.begin(), v.end());
do {
    for (int x : v) std::cout << x << ' ';
    std::cout << '\n';
} while (std::next_permutation(v.begin(), v.end()));

STL 컨테이너와 자주 쓰는 함수

컨테이너 선택

요구사항컨테이너시간복잡도
순서 유지, 인덱스 접근vector접근 O(1), 끝 삽입 분할상환 O(1), 중간 삽입/삭제 O(N)
중복 없는 정렬된 집합set삽입/삭제/탐색 O(log N)
중복 없는 집합, 순서 무관unordered_set평균 O(1), 최악 O(N)
정렬된 키-값mapO(log N)
순서 무관 키-값unordered_map평균 O(1), 최악 O(N)
FIFO / LIFOqueue / stackO(1)
최댓값 우선 꺼내기priority_queue삽입/삭제 O(log N), 최댓값 조회 O(1)

priority_queue는 기본이 최대 힙이므로, 다익스트라처럼 최솟값이 필요하면 std::priority_queue<T, std::vector<T>, std::greater<T>>로 선언합니다.

vector, map, set

std::vector<int> v = {1, 2, 3};
v.push_back(4);           // 끝에 추가
v.pop_back();             // 끝에서 제거
v[0];                     // 범위 검사 없음
v.at(0);                  // 범위를 벗어나면 std::out_of_range 예외

std::map<std::string, int> m;
m["apple"] = 1;
if (m.count("apple")) {           // 존재 여부 (operator[]는 없는 키를 삽입하므로 확인용으로 쓰지 않음)
    std::cout << m["apple"] << '\n';
}
for (auto& [key, val] : m) {      // 키 순서대로 순회
    std::cout << key << ": " << val << '\n';
}

std::set<int> s = {3, 1, 4, 1, 5};  // {1, 3, 4, 5}
s.insert(2);
s.erase(3);
bool exists = s.count(4);  // true

algorithm 헤더

#include <algorithm>

std::sort(v.begin(), v.end());                    // 오름차순
std::sort(v.begin(), v.end(), std::greater<>());  // 내림차순
std::reverse(v.begin(), v.end());

auto it = std::find(v.begin(), v.end(), 42);      // 선형 탐색 O(N)
if (it != v.end()) { /* 찾음 */ }
bool found = std::binary_search(v.begin(), v.end(), 42);  // 정렬된 범위에서만 의미 있음

int maxVal = *std::max_element(v.begin(), v.end());  // 빈 범위면 end()를 역참조하므로 주의

// 중복 제거: unique는 인접한 중복만 뒤로 밀어 두므로 먼저 정렬하고 erase까지 해야 함
std::sort(v.begin(), v.end());
v.erase(std::unique(v.begin(), v.end()), v.end());

// k번째(0부터 셈) 원소를 제자리에 놓음, 평균 O(N)
std::nth_element(v.begin(), v.begin() + k, v.end());

string

std::string s = "hello";
s.substr(1, 3);               // "ell" (시작 인덱스, 길이)
s.find("ll");                 // 2, 없으면 std::string::npos
s.rfind("l");                 // 3 (뒤에서부터 찾기)

int num = std::stoi("123");
long long big = std::stoll("123456789012");
std::string str = std::to_string(123);

문자열에 한 글자씩 붙일 때 s = s + c는 매번 새 문자열을 만들어 전체가 O(N²)이 되므로 s += c나 push_back을 씁니다.


입력 크기로 허용 복잡도 가늠하기

lower_bound·upper_bound 결과, fib(92)와 fib(93)의 long long 범위, 순수 재귀 fib 호출 수, N=10^6 정렬과 10^9번 반복, N=30000 이중 반복의 실측 시간

그림: 실측 결과(Apple Silicon 맥북, -O2) — lower_bound(3)은 인덱스 2이고 upper_bound - lower_bound는 1의 개수 2다. fib(92)는 long long에 들어가고 fib(93)은 long long 최대값을 넘는다. 순수 재귀 fib(40)은 3.3억 번 호출되고, 점화식으로 센 fib(50)의 호출 수는 약 407억 번이다. 10^6개 정렬은 15ms, 단순 연산 10^9번은 약 0.75초, 약 4.5x10^8번의 이중 반복은 57ms였다. 이 맥은 채점 서버보다 빠를 수 있어 글의 기준(초당 10^8)처럼 보수적으로 잡는 것이 안전하다. (환경: macOS 27.0.1 · Apple clang 21.0.0 -O2 · Apple Silicon 맥북)

복잡도N=100N=10,000N=1,000,000예시 알고리즘
O(1)111해시맵 접근(평균)
O(log N)71420이진 탐색
O(N)10010,0001,000,000선형 탐색, 순회
O(N log N)약 700약 130,000약 2×10⁷정렬
O(N²)10,00010⁸10¹²이중 반복문
O(2^N)약 1.3×10³⁰--부분집합 완전 탐색

C++에서 단순한 연산은 1초에 대략 10⁸~10⁹번 처리된다고 보고, 메모리 접근과 분기 비용을 고려해 보수적으로 10⁸ 정도로 잡는 것이 일반적인 기준입니다. 이 기준으로 문제의 N 범위에서 허용 복잡도를 거꾸로 추정할 수 있습니다.

  • N ≤ 10 정도: O(N!) 완전 탐색(순열)
  • N ≤ 20 정도: O(2^N) 부분집합 탐색, 비트마스크 DP
  • N ≤ 500 정도: O(N³) 플로이드-워셜, 구간 DP
  • N ≤ 5,000~10,000 정도: O(N²)
  • N ≤ 10⁶ 정도: O(N log N) 정렬, 세그먼트 트리
  • N이 그보다 크면: O(N) 또는 O(log N)

예를 들어 N = 10⁶에 O(N²) 알고리즘을 쓰면 연산이 10¹²번이라 초당 10⁹번으로 잡아도 1,000초가 걸리지만, O(N log N)이면 약 2×10⁷번이라 제한 시간 안에 들어갑니다. 상수 배와 캐시 효율에 따라 경계는 달라지므로, 경계에 걸리면 연산 수를 직접 세어 보는 것이 좋습니다.


입출력, 엣지 케이스, 메모리

입출력 최적화

백준처럼 표준 입출력을 쓰는 온라인 저지에서는 입출력이 많은 문제에서 cin/cout 기본 설정 때문에 시간 초과가 날 수 있습니다.

#include <bits/stdc++.h>
using namespace std;

int main() {
    ios_base::sync_with_stdio(false);  // C 표준 입출력과의 동기화 해제
    cin.tie(nullptr);                  // cin 전에 cout을 자동 flush하지 않음

    int n;
    cin >> n;

    cout << n << '\n';     // 줄바꿈만
    // cout << n << endl;  // 줄바꿈 + flush
    return 0;
}

sync_with_stdio(false): 기본 상태에서 C++ 스트림은 C의 stdio와 동기화되어 있어 cin과 scanf를 섞어 써도 순서가 맞습니다. 대신 C++ 스트림이 자체 버퍼링을 하지 못해 느립니다. 동기화를 끄면 C++ 스트림이 독립적으로 버퍼링하므로 빨라지지만, 이후로는 cin/cout과 scanf/printf를 섞어 쓰면 출력 순서가 뒤섞일 수 있습니다.

cin.tie(nullptr): 기본적으로 cin은 cout에 묶여 있어서 입력을 읽기 전에 cout 버퍼를 비웁니다. 대화형 프로그램에서 프롬프트가 먼저 보이게 하려는 장치인데, 입력과 출력이 번갈아 나오는 문제에서는 매번 flush가 일어나 느려집니다. 다만 인터랙티브 문제(채점기와 주고받는 문제)에서는 출력 후 반드시 flush해야 하므로 이 설정과 '\n'만 쓰면 안 됩니다.

endl과 '\n': endl은 줄바꿈 후 버퍼를 비우므로, 출력 줄이 수십만 개면 그만큼 시스템 호출이 일어납니다. '\n'은 버퍼가 찰 때나 프로그램이 끝날 때 한 번에 내보냅니다.

구체적인 측정과 더 빠른 입력 방법은 C++ I/O 병목 줄이기에서 다룹니다.

백준과 프로그래머스의 차이

두 플랫폼은 코드를 제출하는 형태가 다릅니다.

#include <iostream>

// 백준: main에서 표준 입력을 직접 읽고 표준 출력으로 답을 씀
int main() {
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr);
    int n;
    std::cin >> n;
    std::cout << n * 2 << '\n';
}

// 프로그래머스: 정해진 시그니처의 solution 함수만 구현 (입출력은 채점기가 처리)
int solution(int n) {
    return n * 2;
}

백준에 익숙한 사람이 프로그래머스에서 자주 하는 실수는 solution 안에서 cin으로 입력을 읽으려는 것이고, 반대로 프로그래머스에 익숙하면 백준에서 입출력 최적화 두 줄을 잊어 시간 초과가 납니다. solution에서 전역 변수를 쓴다면 이전 호출의 값이 남아 있을 가능성에 대비해 함수 시작에서 초기화하는 습관을 들이는 것이 안전합니다.

엣지 케이스

제출 전에 확인할 경우는 대체로 정해져 있습니다. 입력이 비었거나 N이 1인 경우, N이 최대인 경우, 모든 원소가 같은 경우, 음수와 0이 섞인 경우, 그리고 곱이나 합이 int 범위(약 ±2.1×10⁹)를 넘는 경우입니다.

// N=0이면 v[0] 접근이 미정의 동작
int maxVal = v[0];
for (int x : v) maxVal = std::max(maxVal, x);

// 빈 배열을 먼저 처리
if (v.empty()) return -1;
int maxVal2 = *std::max_element(v.begin(), v.end());

오버플로는 곱셈에서 특히 자주 납니다. int a = 1000000, b = 1000000; int r = a * b;는 결과가 2^31을 넘어 미정의 동작이고, long long r = a * b;처럼 결과만 long long에 담아도 곱셈 자체는 int로 이미 끝났으므로 소용없습니다. (long long)a * b처럼 피연산자 하나를 먼저 넓혀야 합니다.

메모리 초과와 스택 오버플로

#include <vector>

int main() {
    int visited[1000000];            // 지역 배열 4MB: 스택 한도를 넘으면 런타임 에러
}

int visited[1000000];                // 전역 배열은 정적 영역에 잡히고 0으로 초기화됨
std::vector<int> visited2(1000000);  // 힙에 할당

지역 변수는 스택에 잡히는데, 기본 스택 크기는 Windows가 1MB, Linux가 보통 8MB라 큰 지역 배열이 로컬에서는 되고 채점 서버에서는 안 되거나 그 반대가 됩니다. 큰 배열은 전역이나 vector로 두고, 참/거짓만 담는 큰 배열은 std::vector<bool>이나 std::bitset으로 바꾸면 bool 배열보다 메모리가 8분의 1로 줄어듭니다. 깊이가 수십만인 DFS 재귀도 같은 이유로 스택을 넘칠 수 있으므로, 입력 한도가 크면 명시적 스택을 쓰는 반복문으로 바꾸는 것을 고려합니다.

구현(시뮬레이션) 문제

백준에는 알고리즘보다 조건을 빠짐없이 코드로 옮기는 능력을 보는 유형이 있습니다. 주사위 굴리기(14499), 로봇 청소기(14503)처럼 격자 위에서 규칙대로 움직이는 문제가 대표적입니다. 이런 문제는 방향 배열(dx, dy)의 순서, 회전 방향, 범위 검사를 이동 전에 하는지 후에 하는지 같은 세부 사항에서 틀리는 경우가 대부분입니다. 코드를 쓰기 전에 문제의 규칙을 번호 목록으로 옮겨 적고 각 규칙을 함수 하나씩으로 구현하면 디버깅이 쉬워지고, 막히면 매 단계 후 격자 상태를 출력해 손으로 푼 결과와 비교합니다.


면접관 앞에서 풀 때

면접 형식의 코딩 테스트에서는 정답만큼 풀이 과정을 설명하는 방식이 평가됩니다. 먼저 입력 범위와 출력 형식을 질문으로 확인하고(“입력은 N개의 정수이고, 합이 K인 두 원소의 인덱스를 반환하면 되나요?”), 바로 코드를 쓰기보다 무식한 방법과 그 복잡도를 말한 뒤 개선 방향을 설명합니다.

복잡도 질문에는 단계별로 나누어 답합니다. “정렬에 O(N log N), 이후 투 포인터로 O(N)이므로 전체 O(N log N)입니다”처럼 말하고, 공간복잡도도 함께 언급합니다. 해시맵 풀이는 시간 O(N)에 추가 공간 O(N)을 쓰고, 정렬과 투 포인터 풀이는 시간 O(N log N)에 추가 공간이 정렬의 재귀 스택 정도(O(log N))라는 트레이드오프를 설명할 수 있으면 좋습니다. 막혔을 때는 무엇을 고민하는지 구체적으로 말하고 힌트를 요청하는 편이 침묵하는 것보다 낫습니다.

연습은 프로그래머스 레벨 1~2와 LeetCode의 유형별 목록으로 유형을 익히고(오랫동안 많이 쓰이던 백준은 2026년 4월 28일 채점 서비스를 종료하고 재개를 준비 중이라 지금은 쓸 수 없습니다. 이 글의 백준 문제 번호는 유형 표시로 보면 됩니다), 지원하는 회사의 테스트 형식(프로그래머스 기반인지, 자체 플랫폼인지, 면접관 동석인지)을 확인해 그 형식으로 시간을 재고 풀어 보는 것이 효과적입니다. 해외 기업을 준비한다면 LeetCode가, 알고리즘 실력 자체를 키우려면 Codeforces 같은 대회형 사이트가 맞습니다.