핵심 알고리즘 유형별 해결 방안과 코드 리팩토링

부제: 기본적인 수학 논리와 자료구조를 통한 효율적 설계

다음 내용은 특정 알고리즘 대회에서 자주 등장하는 4 가지 핵심 유형에 대한 접근법과 개선된 구현 예시를 다룹니다. 각 문제는 수학적 성질 검증, 그리디(Greedy) 할당, 해싱 기반 카운팅, 그리고 동적 계획법(DP) 과 재귀 탐색을 포함하고 있습니다.

1. 짝수 합분해 가능성 판정

목표: 주어진 양의 정수가 두 개의 양의 짝수 정수의 합으로 표현될 수 있는지 확인해야 합니다.

논리적 접근:
임의의 양의 짝수 $a$ 와 $b$ 를 가정할 때, 그 합 $S = a + b$ 역시 반드시 짝수가 됩니다. 또한, 가장 작은 양의 짝수는 2 이므로 가능한 최소 합은 $2+2=4$ 가 되어야 합니다.
따라서 다음 두 조건을 동시에 충족해야만 가능함을 알 수 있습니다:

  • 수 $S$ 는 짝수여야 합니다.
  • 수 $S$ 는 최소 4 이상이어야 합니다.

이를 바탕으로 입력 값을 검증하는 로직을 작성합니다.

#include <iostream>
#include <string>

using namespace std;

// 주어진 수가 두 양의 짝수의 합인지 반환
bool isSumOfTwoPosEvens(int targetNum) {
    // 홀수는 불가능하며, 2 는 두 양의 짝수(최소 2+2=4) 의 합으로 만들 수 없음
    return (targetNum % 2 == 0 && targetNum >= 4);
}

void solve() {
    int inputValue;
    if (!(cin >> inputValue)) return;
    
    if (isSumOfTwoPosEvens(inputValue)) {
        cout << "Possible\n";
    } else {
        cout << "Impossible\n";
    }
}

int main() {
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);
    solve();
    return 0;
}

2. 구간 제약 하의 배열 합 구성

목표: $N$ 개 요소의 배열을 생성하되, 각 요소는 주어진 최소/최대 범위 내에서 선택되어야 하며 전체 합이 특정 목표값과 일치해야 합니다.

해결 전략:
우선 모든 위치를 최소값으로 채웠을 때의 총합 ($MinSum$) 과 최대값으로 채웠을 때의 총합 ($MaxSum$) 을 계산합니다. 목표 합이 이 구간에 포함되지 않으면 해가 존재하지 않습니다.
존재한다면, 기본적으로 최소값 배열을 시작점으로 삼고, 부족하거나 남는 합치를 앞에서부터 순차적으로 보충해 나가면 됩니다. 이는 그리디한 접근 방식으로 항상 성공 가능한 조합을 찾을 수 있습니다.

#include <iostream>
#include <vector>
#include <numeric>

using namespace std;

struct TimeSlot {
    int minDuration, maxDuration;
};

int main() {
    int days, requiredSum;
    cin >> days >> requiredSum;

    vector<TimeSlot> slots(days);
    int minTotal = 0, maxTotal = 0;

    for (int i = 0; i < days; ++i) {
        cin >> slots[i].minDuration >> slots[i].maxDuration;
        minTotal += slots[i].minDuration;
        maxTotal += slots[i].maxDuration;
    }

    if (requiredSum < minTotal || requiredSum > maxTotal) {
        cout << "No Solution\n";
        return 0;
    }

    vector<int> result(days);
    int currentRemainder = requiredSum - minTotal;

    cout << "Valid Assignment:\n";
    for (int i = 0; i < days; ++i) {
        result[i] = slots[i].minDuration;
        int addable = slots[i].maxDuration - slots[i].minDuration;
        
        // 추가로 배분 가능한 만큼 넣거나 나머지가 떨어질 때까지 분배
        int give = min(currentRemainder, addable);
        result[i] += give;
        currentRemainder -= give;
        
        cout << result[i] << " ";
    }
    cout << "\n";

    return 0;
}

3. 문자열 등록 및 중복 방지 메커니즘

목표: 여러 번의 입력 문자열이 들어올 때, 처음 본다면 성공을 출력하고, 이미 등록된 문자열이라면 숫자 접미사를 붙여 구분하여 새로운 식별자로 등록합니다.

자료구조 활용:
문자열의 등장 빈도를 기록하기 위해 해시 맵 (Hash Map) 이나 트리 기반 맵 (Tree Map) 을 사용하는 것이 효율적입니다. 각 문자열이 몇 번째로 호출되었는지 인덱스를 관리하여 중복 시 자동으로 증가된 ID 를 생성합니다.

#include <iostream>
#include <string>
#include <unordered_map>

using namespace std;

void processQueries(int queryCount) {
    // 문자열과 등장 횟수를 매핑
    unordered_map<string, int> occurrenceTracker;
    
    while (queryCount--) {
        string incomingText;
        cin >> incomingText;

        auto& countRef = occurrenceTracker[incomingText];
        if (countRef == 0) {
            // 첫 출현
            cout << "Accepted\n";
            countRef++;
        } else {
            // 중복 발생: 기존 문자열 + 현재 번호 출력
            cout << incomingText << countRef << "\n";
            countRef++;
        }
    }
}

int main() {
    // 입출력 속도 최적화
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);

    int n;
    cin >> n;
    processQueries(n);

    return 0;
}

4. 2 차원 제한 조건 하의 최장 경로 탐색

목표: 주어진 항목들 중에서 제거 후에도 유지되는, 두 가지 속성이 모두 엄격하게 증가하는 최장 부분 수열의 길이를 찾고 실제 경로를 복원해야 합니다. 여기서 특정 기준치 이하의 값들은 유효하지 않은 것으로 간주됩니다.

구현 논리:
이는 긴 공통 부분 수열 (LCS) 의 변형인, 2 차원 증가 수열 문제로 볼 수 있습니다. $O(N^2)$ 에 해결 가능한 메모이제이션 된 깊이 우선 탐색 (Memoized DFS) 을 사용하도록 설계했습니다.
각 노드에서 다른 노드로 이동할 수 있는지는 해당 노드의 두 차원 값이 현재 노드보다 모두 커야 하며, 사전 필터링된 무효 노드는 제외합니다.

#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

const int MAX_N = 5005;

struct Element {
    int idx;
    int width, height;
    bool isValid;
};

Element data[MAX_N];
int memo[MAX_N];
int nextNodeIdx[MAX_N];
int totalItems, limitW, limitH;

// 현재 노드 x 에서 시작할 때의 최대 길이를 반환
int findLongestChain(int currentIdx) {
    int& res = memo[currentIdx];
    if (res != -1) return res;

    res = 1; // 자기 자신 포함한 기본 길이
    
    for (int i = 1; i <= totalItems; ++i) {
        if (!data[i].isValid) continue;

        // 두 차원 모두嚴格增加 해야 연결 가능
        if (data[i].width > data[currentIdx].width && 
            data[i].height > data[currentIdx].height) {
            
            int potentialLen = findLongestChain(i) + 1;
            if (potentialLen > res) {
                res = potentialLen;
                nextNodeIdx[currentIdx] = i;
            }
        }
    }
    return res;
}

int main() {
    cin >> totalItems >> limitW >> limitH;

    // 0 번 인덱스를 더미 루트로 설정 (값 0, 0)
    data[0] = {0, 0, 0, true};

    for (int i = 1; i <= totalItems; ++i) {
        int w, h;
        cin >> w >> h;
        data[i] = {i, w, h, true};
        
        // 제약 조건에 맞지 않으면 비활성화
        if (w <= limitW || h <= limitH) {
            data[i].isValid = false;
        }
    }

    // 메모이제이션 테이블 초기화
    fill(memo, memo + MAX_N, -1);
    fill(nextNodeIdx, nextNodeIdx + MAX_N, 0);

    int maxLength = findLongestChain(0) - 1; // 더미 노드 제외
    cout << maxLength << "\n";

    // 경로 복원
    int cursor = nextNodeIdx[0];
    while (cursor != 0) {
        cout << cursor << " ";
        cursor = nextNodeIdx[cursor];
    }
    cout << "\n";

    return 0;
}

태그: C++ algorithm CompetitiveProgramming GreedyApproach Memoization

5월 29일 00:49에 게시됨