https://www.acmicpc.net/problem/2512



예산



아 parametric search 구나


라고는 빨리 알았지만, 이분탐색의 늪에 빠졌다가 뭔가 깨달음을 얻어서 여기에 옮기고자 한다.




k번째 숫자(https://www.acmicpc.net/problem/7469)를 parametric search로 풀고, 이 문제도 parametric search로 풀었다.


전자는


1
2
3
4
5
6
7
8
9
10
int l = -1000000000;
int r = 1000000000;
 
while (l < r) {
    int mid = (l + r) >> 1;
    if (q(10, n - 1, i - 1, j - 1, mid) >= k)
        r = mid;
    else
        l = mid + 1;
}
cs

후자

후자는


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
while (l < r) {
    int mid = (l + r + 1/ 2;
    int sum = 0;
    for (int i = 0; i < n; ++i) {
        if (arr[i] >= mid)
            sum += mid;
        else
            sum += arr[i];
    }
 
    if (sum <= m)
        l = mid;
    else
        r = mid - 1;
}
cs



와 같다.




전자의 경우 mid가 성공했다면, 그 값을 포함한채 오른쪽을 좁혀야 하고,


(k번째 숫자를 찾아야 하므로, 더 큰 값은 의미가 없고, mid는 정답일 수 있으므로 버리면 안된다.)


후자의 경우 mid가 성공했으면, 그 값을 포함한채 왼쪽을 좁혀야 한다.


(최대 예산액을 찾아야 하므로, 작은 값은 의미가 없으며, mid는 정답일 수 있으므로 버리면 안된다.)



따라서 left와 right로 mid를 정하는 기준이 달라지는데, 이유를 말로 하긴 복잡하니까 그림으로 설명해보겠다.



mid 를 (left + right) / 2 로 하느냐, (left + right + 1) / 2로 하느냐의 차이점은, (성공했을 때 좌우를 변경하는 로직이 완벽했을 때)


결국엔 range가 2일 때 선명하게 드러나는데,


이번 예산 문제의 경우 mid 를 (left + right) / 2 로 하고 성공했을 경우, 


위 그림과 같이 무한루프에 빠지게 된다. 



반대로 k번째 숫자 문제는,


mid 를 (left + right + 1) / 2 로 한다면 성공했을 경우 무한루프에 빠지게 된다.



따라서 좌우를 버리는 로직을 완벽히 세워놨다면, 모든 parametric search를 위 두 케이스 중에 알맞은 케이스로 풀면 된다


라는게 내린 결론인데, 평소 while(left <= right)인 코드를 많이 봐왔던지라 차이점이 궁금해지긴 한다.


정파말고 사파식 공부를 하는 중이라 깊이가 모자른걸 요새 크게 통감하고 있다 ㅎㅎ;


풀 코드는 다음과 같다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <stdio.h>
#include <algorithm>
using namespace std;
 
int n, m;
int arr[10000];
 
int main() {
    int l = 1, r = 0;
 
    scanf("%d"&n);
    for (int i = 0; i < n; ++i) {
        scanf("%d"&arr[i]);
        r = max(r, arr[i]);
    }
    scanf("%d"&m);
 
    while (l < r) {
        int mid = (l + r + 1/ 2;
        int sum = 0;
        for (int i = 0; i < n; ++i) {
            if (arr[i] >= mid)
                sum += mid;
            else
                sum += arr[i];
        }
 
        if (sum <= m)
            l = mid;
        else
            r = mid - 1;
    }
    printf("%d", l);
}
cs


https://www.acmicpc.net/problem/12878


Blocks


n=1일때와 n=2일때 어떻게 전개되는지를 생각해보니 금방 풀 수 있었다.


상태

경우의 수

빨강

노랑

추가->

a

c

2a+b+c

a

b

a

b

d

a+2b+d

b

a

b

c

a

a+2c+d

c

d

c

d

b

b+c+2d

d

c

d



따라서 n개일 때 경우의 수들에 행렬을 곱해 n=1일 때 경우의 수들을 구할 수 있다.


그래서 결국 4*4 행렬을 구하는 문제로 바뀌는데, n이 십억까지므로, 2^x승들의 행렬들을 미리 구하는 분할정복으로 풀 수 있다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#include <iostream>
#include <math.h>
#include <string.h>
using namespace std;
 
#define m 10007
typedef long long ll;
 
ll n;
ll origin[4][4= 
{2,1,1,0},
{1,2,0,1},
{1,0,2,1},
{0,1,1,2} };
 
ll mat[30][4][4];
 
int main() {
    memcpy(mat[0], origin, sizeof(origin));
 
    cin >> n;
    ll tmp[4][4];
    int d = 1;
    while ((ll(1<< d) <= n) {
        for (int i = 0; i < 4++i) {
            for (int j = 0; j < 4++j) {
                ll t = 0;
                for (int k = 0; k < 4++k) {
                    t = (t + (mat[d - 1][i][k] * mat[d - 1][k][j]) % m) % m;
                }
                tmp[i][j] = t;
            }
        }
 
        memcpy(mat[d++], tmp, sizeof(tmp));
    }
 
    int dd = 0;
    int arr[4= { 1,0,0,0 };
    while ((ll(1<< dd) <= n) {
        int tmp[4];
        if (n & (ll(1<< dd)) {
            for (int i = 0; i < 4++i) {
                tmp[i] = 0;
                for (int j = 0; j < 4++j)
                    tmp[i] = (tmp[i] + (mat[dd][i][j] * arr[j]) % m) % m;
            }
 
            memcpy(arr, tmp, sizeof(tmp));
        }
        dd++;
    }
 
    cout << arr[0] % m;
}
cs


잘못 구현한 에라토스테네스의 체




첨엔 거지같이 풀어서 다른 분의 깔삼한 코드에 감명받아 여기에 옮기고자 한다.


cubelover님 감사합니다 ㅜㅜ


N = 12 인 경우에 답은 41인데, 과정을 표로 그려보면 다음과 같다.



i j                          
1 1 2 3 4 5 6 7 8 9 10 11 12 = 12회
2 1 3 5 7 9 11             = 6회
3 1 4 7 10                 = 4회
                        =  
5 1 6 11                   = 3회
                        =  
11 1 12                     = 2회
12 1                        = 1회
                          41회


아래서 부터 과정을 살펴보면,


1부터 12칸씩 뛰는데 한번도 나아갈 수 없다.

1부터 11칸씩 뛰는데 한번만 나아갈 수 있다.

...



횟수에 초점을 맞춰보면, 약수처럼 횟수가 늘어남을 알 수 있다. 따라서 횟수들의 종류 사이마다 i가 몇개씩 있는지만 알면,


n의 약수 갯수와 비스무리한 시간에 풀릴 것이다!



밑에서 부터 간격을 점프해보자.


1부터 시작하므로 최대 점프길이는 11이다. 11/12 = 0 이고, 1을 더하면 1회가 계산된다.


다음 i를 계산하는데는 직관적으로 계산된다. 11 / 1회 = 11이므로 다음 i는 11이다.



이는 1회 = 1번 점프 = 1부터 11까지 최대한 멀리 점프할수 있는 수 = 11 같이 이해하면 될 것 같다.


i = 11일 때 j는 두 번 등장한다. = 2회


다음 i = 11 / 2회 = 5이다.



즉 횟수가 달라지는 i들은(횟수들의 최대 i값들은) 12->11->5->3->2->1 순이다.



점프 횟수와 i들의 간격도 모두 구할 수 있으니 다음 처럼 간단한 코드로 풀이가 마무리된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <cstdio>
 
long long ans;
int n;
 
int main() {
    scanf("%d"&n);
    n--;
 
    for (int i = n + 1; i != 0; i = n / ((n / i) + 1))
        ans += (n / i + 1* (i - (n / ((n / i) + 1)));
 
    printf("%lld\n", ans);
}
 
cs






+ Recent posts