μ•Œκ³ λ¦¬μ¦˜/BOJ - Java

[BOJ/Java] 6902. Orko

ImJay 2025. 3. 13. 17:35
λ°˜μ‘ν˜•

πŸ“Œ [BOJ/Java] 6902. Orko

πŸ”— 문제 링크: 문제 보기


πŸ“– 문제 해석

Dumitruκ°€ μ΄μƒν•œ κΏˆμ„ 꾸게 λœλ‹€. 꿈 μ†μ—μ„œ 문이 잠긴 방에 κ°‡νžŒ DumitruλŠ” n개의 λ°•μŠ€λ₯Ό λ°œκ²¬ν•˜λ©°, 각 λ°•μŠ€μ—λŠ” m개의 μ ‘μ‹œκ°€ μžˆλ‹€. 각 μ ‘μ‹œμ—λŠ” 1 μ΄μƒμ˜ μ •μˆ˜κ°€ μ ν˜€ μžˆλ‹€. 그러던 쀑, 두 개의 μ •μˆ˜ k와 l이 적힌 μͺ½μ§€λ₯Ό λ°œκ²¬ν•˜κ³ , μͺ½μ§€μ—λŠ” μ•„λž˜μ™€ 같은 μž‘μ—…μ„ ν•˜λΌλŠ” μ§€μ‹œκ°€ μ ν˜€ μžˆλ‹€.

 

1단계: 첫 번째 λ°•μŠ€λΆ€ν„° n번째 λ°•μŠ€κΉŒμ§€ μˆœμ„œλŒ€λ‘œ 각 λ°•μŠ€μ—μ„œ ν•œ 개의 μ ‘μ‹œλ₯Ό μ„ νƒν•œλ‹€. μ„ νƒν•œ μ ‘μ‹œμ˜ 숫자λ₯Ό λ…ΈνŠΈμ— κΈ°λ‘ν•œ ν›„, ν•΄λ‹Ή μ ‘μ‹œμ˜ 숫자λ₯Ό 1둜 λ³€κ²½ν•œλ‹€.

 

2단계: n-1번째 λ°•μŠ€λΆ€ν„° 두 번째 λ°•μŠ€κΉŒμ§€ μ—­μˆœμœΌλ‘œ 각 λ°•μŠ€μ—μ„œ ν•œ 개의 μ ‘μ‹œλ₯Ό μ„ νƒν•œλ‹€.

λͺ©ν‘œλŠ” λ…ΈνŠΈμ— 기둝된 μˆ«μžλ“€μ˜ 곱이 k둜 λ‚˜λˆ„μ–΄ 떨어지도둝 ν•˜λŠ” 선택 λ°©λ²•μ˜ 총 κ°€μ§“μˆ˜ Tλ₯Ό κ΅¬ν•˜κ³ , Tλ₯Ό l둜 λ‚˜λˆˆ λ‚˜λ¨Έμ§€λ₯Ό 좜λ ₯ν•˜λŠ” 것이닀.


πŸ› οΈ 풀이 κ³Όμ •

λ¨Όμ €, n, m, k, lκ³Ό 각 λ°•μŠ€μ˜ μ ‘μ‹œ 값듀을 μž…λ ₯λ°›λŠ”λ‹€. 문제의 핡심은 k의 μ•½μˆ˜λ₯Ό μƒνƒœλ‘œ ν™œμš©ν•˜λŠ” 것이닀. k의 λͺ¨λ“  μ•½μˆ˜λ₯Ό κ΅¬ν•œ λ’€, 각 μ•½μˆ˜μ— 인덱슀λ₯Ό λ§€ν•‘ν•˜μ—¬ μƒνƒœ 전이 ν…Œμ΄λΈ”μ„ κ΅¬μ„±ν•œλ‹€.

 

각 λ°•μŠ€λ§ˆλ‹€ μ ‘μ‹œ μ„ νƒμ˜ 경우의 수λ₯Ό κ³„μ‚°ν•œλ‹€. 첫 번째 λ°•μŠ€μ™€ λ§ˆμ§€λ§‰ λ°•μŠ€λŠ” μ ‘μ‹œλ₯Ό ν•œ 번만 μ„ νƒν•˜λ―€λ‘œ, 각 μ ‘μ‹œκ°€ κΈ°μ—¬ν•˜λŠ” κ°’ κ·ΈλŒ€λ‘œ λ°˜μ˜λœλ‹€.

 

쀑간 λ°•μŠ€λŠ” μ ‘μ‹œλ₯Ό 두 번 μ„ νƒν•˜λŠ” κ²½μš°κ°€ μžˆμœΌλ―€λ‘œ, 첫 번째 선택(μ ‘μ‹œμ˜ μ›λž˜ 숫자 기둝)κ³Ό 두 번째 선택(μ ‘μ‹œ μˆ«μžκ°€ 1둜 변경됨)을 λͺ¨λ‘ κ³ λ €ν•˜μ—¬ 경우의 수λ₯Ό λˆ„μ ν•œλ‹€.

 

동적 κ³„νšλ²•(DP)을 μ΄μš©ν•΄ 각 λ°•μŠ€μ—μ„œ κ³„μ‚°λœ 경우의 수λ₯Ό λˆ„μ ν•˜λ©°, ν˜„μž¬κΉŒμ§€ μ„ νƒν•œ μ ‘μ‹œλ“€μ˜ 곱의 gcd(k, ·) 값을 μƒνƒœλ‘œ ν‘œν˜„ν•˜μ—¬ 전이λ₯Ό μˆ˜ν–‰ν•œλ‹€.

 

λͺ¨λ“  λ°•μŠ€λ₯Ό μ²˜λ¦¬ν•œ ν›„, μ΅œμ’… μƒνƒœκ°€ k(즉, k둜 λ‚˜λˆ„μ–΄ λ–¨μ–΄μ§€λŠ” μƒνƒœ)κ°€ λ˜λŠ” 경우의 수λ₯Ό l둜 λ‚˜λˆˆ λ‚˜λ¨Έμ§€λ₯Ό 좜λ ₯ν•œλ‹€.


πŸ’» μ½”λ“œ κ΅¬ν˜„ (Java)

import java.io.*; 
import java.util.*;

public class Main {

// μœ ν΄λ¦¬λ“œ ν˜Έμ œλ²•μ„ μ΄μš©ν•˜μ—¬ μ΅œλŒ€κ³΅μ•½μˆ˜(GCD)λ₯Ό κ΅¬ν•˜λŠ” λ©”μ†Œλ“œ
static long gcd(long a, long b) {
    // bκ°€ 0이 될 λ•ŒκΉŒμ§€ 반볡
    while (b != 0) {
        long temp = a % b;  // aλ₯Ό b둜 λ‚˜λˆˆ λ‚˜λ¨Έμ§€λ₯Ό μž„μ‹œ μ €μž₯
        a = b;              // a에 b 값을 λŒ€μž…
        b = temp;           // b에 λ‚˜λ¨Έμ§€ 값을 λŒ€μž…
    }
    return a;             // μ΅œλŒ€κ³΅μ•½μˆ˜ λ°˜ν™˜
}

public static void main(String[] args) throws Exception {
    // μž…λ ₯을 μœ„ν•œ BufferedReader μ΄ˆκΈ°ν™”
    BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
    
    // 첫 번째 쀄 μž…λ ₯: λ°•μŠ€ 개수 nκ³Ό 각 λ°•μŠ€λ‹Ή μ ‘μ‹œ 개수 m
    StringTokenizer st = new StringTokenizer(br.readLine());
    int n = Integer.parseInt(st.nextToken());
    int m = Integer.parseInt(st.nextToken());
    
    // 두 번째 쀄 μž…λ ₯: λͺ©ν‘œ μ •μˆ˜ k와 λͺ¨λ“ˆλŸ¬ 연산에 μ‚¬μš©ν•  κ°’ mod
    st = new StringTokenizer(br.readLine());
    int k = Integer.parseInt(st.nextToken());
    int mod = Integer.parseInt(st.nextToken());
    
    // k의 λͺ¨λ“  μ•½μˆ˜λ₯Ό ꡬ함 (각 μƒνƒœλ‘œ ν™œμš©)
    ArrayList<Integer> dList = new ArrayList<>();
    for (int i = 1; i * i <= k; i++) {
        if (k % i == 0) {
            dList.add(i);
            if (i * i != k) {
                dList.add(k / i);
            }
        }
    }
    
    // μ•½μˆ˜λ₯Ό μ˜€λ¦„μ°¨μˆœμœΌλ‘œ μ •λ ¬
    Collections.sort(dList);
    int dCount = dList.size();
    
    // μ•½μˆ˜λ₯Ό 배열에 μ €μž₯
    int[] dArr = new int[dCount];
    for (int i = 0; i < dCount; i++) {
        dArr[i] = dList.get(i);
    }
    
    // 각 μ•½μˆ˜μ— λŒ€ν•΄ 인덱슀λ₯Ό λ§€ν•‘ (μƒνƒœ 전이 ν…Œμ΄λΈ” ꡬ성에 ν™œμš©)
    HashMap<Integer, Integer> dIndex = new HashMap<>();
    for (int i = 0; i < dCount; i++) {
        dIndex.put(dArr[i], i);
    }
    
    // μƒνƒœ 전이 ν…Œμ΄λΈ” ꡬ성: 두 μƒνƒœμ˜ 곱의 결과에 λŒ€ν•΄ gcd(k, κ³±) 값을 미리 계산
    int[][] mult = new int[dCount][dCount];
    for (int i = 0; i < dCount; i++) {
        for (int j = 0; j < dCount; j++) {
            long product = (long) dArr[i] * dArr[j];
            int g = (int) gcd(product, k);
            mult[i][j] = dIndex.get(g);
        }
    }
    
    // DP λ°°μ—΄ μ΄ˆκΈ°ν™”: dp[i]λŠ” ν˜„μž¬ μƒνƒœκ°€ dArr[i]일 λ•Œμ˜ 경우의 수
    int[] dp = new int[dCount];
    // 초기 μƒνƒœλŠ” μ ‘μ‹œ 선택 μ „ 곱이 1인 μƒνƒœ
    dp[dIndex.get(1)] = 1;
    
    // 각 λ°•μŠ€λ₯Ό μˆœμ„œλŒ€λ‘œ 처리
    for (int box = 1; box <= n; box++) {
        
        // ν˜„μž¬ λ°•μŠ€μ˜ μ ‘μ‹œλ“€μ„ 읽어, 각 μ ‘μ‹œμ˜ μˆ«μžμ— λ”°λ₯Έ μƒνƒœ(μ•½μˆ˜ μƒνƒœ)의 λΉˆλ„μˆ˜λ₯Ό 계산
        int[] freq = new int[dCount];
        st = new StringTokenizer(br.readLine());
        for (int j = 0; j < m; j++) {
            int plateValue = Integer.parseInt(st.nextToken());
            int g = (int) gcd(plateValue, k);
            int idx = dIndex.get(g);
            freq[idx]++;  // ν•΄λ‹Ή μƒνƒœμ˜ λΉˆλ„μˆ˜ 증가
        }
        
        // ν˜„μž¬ λ°•μŠ€μ—μ„œ 선택 κ°€λŠ₯ν•œ 경우의 수 뢄포λ₯Ό μ €μž₯ν•  λ°°μ—΄
        int[] boxDist = new int[dCount];
        
        // 첫 번째 λ°•μŠ€μ™€ λ§ˆμ§€λ§‰ λ°•μŠ€λŠ” μ ‘μ‹œλ₯Ό ν•œ 번만 μ„ νƒν•˜λ―€λ‘œ λ‹¨μˆœνžˆ freq 값을 μ‚¬μš©
        if (box == 1 || box == n) {
            for (int i = 0; i < dCount; i++) {
                boxDist[i] = freq[i] % mod;
            }
        } else {
            // 쀑간 λ°•μŠ€λŠ” μ ‘μ‹œλ₯Ό 두 번 선택해야 함
            // 첫 번째 선택: μ ‘μ‹œμ˜ μ›λž˜ 숫자λ₯Ό 기둝
            // 두 번째 선택: ν•΄λ‹Ή μ ‘μ‹œμ˜ μˆ«μžκ°€ 1둜 변함
            int[] doubleDist = new int[dCount];
            
            // 첫 번째 μ„ νƒμ˜ 경우의 수λ₯Ό μΆ”κ°€
            for (int i = 0; i < dCount; i++) {
                doubleDist[i] = (doubleDist[i] + freq[i]) % mod;
            }
            
            // 두 번째 선택에 λŒ€ν•΄, 같은 μ ‘μ‹œλ₯Ό 두 번 μ„ νƒν•˜λŠ” κ²½μš°μ™€ λ‹€λ₯Έ μ ‘μ‹œλ₯Ό μ„ νƒν•˜λŠ” 경우λ₯Ό λͺ¨λ‘ κ³ λ €
            for (int i = 0; i < dCount; i++) {
                for (int j = 0; j < dCount; j++) {
                    long ways;
                    if (i == j) {
                        // 같은 μ ‘μ‹œλ₯Ό 두 번 μ„ νƒν•˜λŠ” 경우
                        ways = (long) freq[i] * (freq[i] - 1);
                    } else {
                        // μ„œλ‘œ λ‹€λ₯Έ μ ‘μ‹œλ₯Ό μ„ νƒν•˜λŠ” 경우
                        ways = (long) freq[i] * freq[j];
                    }
                    if (ways == 0)
                        continue;
                    
                    // 두 μ„ νƒμ˜ κ²°κ³Ό μƒνƒœλ₯Ό μƒνƒœ 전이 ν…Œμ΄λΈ”μ„ μ΄μš©ν•΄ 계산
                    int newIndex = mult[i][j];
                    doubleDist[newIndex] = (int) ((doubleDist[newIndex] + ways) % mod);
                }
            }
            boxDist = doubleDist;
        }
        
        // ν˜„μž¬κΉŒμ§€μ˜ dp λ°°μ—΄κ³Ό ν˜„μž¬ λ°•μŠ€μ˜ 경우의 수(boxDist)λ₯Ό μ΄μš©ν•˜μ—¬ μƒνƒœ 전이λ₯Ό μˆ˜ν–‰
        int[] nextDP = new int[dCount];
        for (int i = 0; i < dCount; i++) {
            if (dp[i] == 0)
                continue;
            for (int j = 0; j < dCount; j++) {
                if (boxDist[j] == 0)
                    continue;
                int nextState = mult[i][j];  // μƒνƒœ 전이
                nextDP[nextState] = (int) ((nextDP[nextState] + (long) dp[i] * boxDist[j]) % mod);
            }
        }
        
        // dp 배열을 μ—…λ°μ΄νŠΈ
        dp = nextDP;
    }
    
    // μ΅œμ’…μ μœΌλ‘œ λͺ©ν‘œ μƒνƒœ k(즉, k둜 λ‚˜λˆ„μ–΄ λ–¨μ–΄μ§€λŠ” μƒνƒœ)의 경우의 수λ₯Ό mod둜 λ‚˜λˆˆ λ‚˜λ¨Έμ§€λ₯Ό 좜λ ₯
    System.out.println(dp[dIndex.get(k)] % mod);
}

⏱️ μ‹œκ°„ λ³΅μž‘λ„ 뢄석

λ¬Έμ œλŠ” 각 λ°•μŠ€λ§ˆλ‹€ m개의 μ ‘μ‹œλ₯Ό μ²˜λ¦¬ν•˜κ³ , k의 μ•½μˆ˜ 개수λ₯Ό d라 ν•  λ•Œ 전체 μ‹œκ°„ λ³΅μž‘λ„λŠ” O(n*m + n*d²)이닀.
n은 μ΅œλŒ€ 200, m은 μ΅œλŒ€ 10,000이며, dλŠ” k의 μ•½μˆ˜ 개수둜 보톡 √k에 κ°€κΉŒμ›Œ μ œν•œ λ‚΄μ—μ„œ μΆ©λΆ„νžˆ 계산 κ°€λŠ₯ν•˜λ‹€.


πŸ’‘ λŠλ‚€μ 

문제 ν•΄κ²° κ³Όμ •μ—μ„œ 각 λ°•μŠ€μ—μ„œ μ ‘μ‹œ 선택에 λ”°λ₯Έ μƒνƒœ 전이λ₯Ό λͺ…ν™•ν•˜κ²Œ μ²˜λ¦¬ν•˜λŠ” 것이 ν•΅μ‹¬μ΄μ—ˆλ‹€.
μ •μˆ˜λ‘ κ³Ό gcdλ₯Ό ν™œμš©ν•˜μ—¬ k의 μ•½μˆ˜λ₯Ό μƒνƒœλ‘œ μ²˜λ¦¬ν•œ 점, 그리고 쀑간 λ°•μŠ€μ—μ„œ 두 번 μ„ νƒν•˜λŠ” 경우λ₯Ό μ„Έλ°€ν•˜κ²Œ κ³ λ €ν•œ 점이 μΈμƒμ μ΄μ—ˆλ‹€.

λ°˜μ‘ν˜•