페이지

레이블이 dynamic programming인 게시물을 표시합니다. 모든 게시물 표시
레이블이 dynamic programming인 게시물을 표시합니다. 모든 게시물 표시

11503번: Tree Edit

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

T1, T2의 각 노드에 서로 다른 번호가 부여되어 있다고 하자.
i번 노드의 자식들을 ci_1, ci_2, ...라고 하자.
ed[i][j]: i번 노드가 루트인 T1의 서브트리를 j번 노드가 루트인 T2의 서브트리로 바꾸는데 필요한 최소 편집 연산 수
ed[i][j]는 ed[ci_1..n][cj_1..m]들을 가지고 DP로 구할 수 있다.

dp[u][v]: ci_1..u 를 cj_1..v로 바꾸는데 필요한 최소 편집 연산 수
dp[u][v] = min(
dp[u-1][v] + (ci_u가 루트인 서브트리의 사이즈),
dp[u][v-1] + (cj_v가 루트인 서브트리의 사이즈),
dp[u-1][v-1] + ed[ci_u][cj_v])
그러면 ed[i][j]는 dp[n][m]이 된다.

시간복잡도는 $O(n^2)$

#include<cstdio>
#include<vector>
#include<algorithm>
using namespace std;
int tc, ed[2000][2000], sz[2000], pos, id;
char la[2000], s[3001];
vector<int> adj[2000];
int f(int l, int r) {
    if (~ed[l][r]) return ed[l][r];
    int n = adj[l].size(), m = adj[r].size();
    vector<vector<int> > dp(n + 1, vector<int>(m + 1));
    for (int i = 1; i <= n; i++) dp[i][0] = dp[i - 1][0] + sz[adj[l][i - 1]];
    for (int i = 1; i <= m; i++) dp[0][i] = dp[0][i - 1] + sz[adj[r][i - 1]];
    for (int i = 1; i <= n; i++) for (int j = 1; j <= m; j++)
        dp[i][j] = min({ dp[i][j - 1] + sz[adj[r][j - 1]],
            dp[i - 1][j] + sz[adj[l][i - 1]],
            dp[i - 1][j - 1] + f(adj[l][i - 1], adj[r][j - 1]) });
    return ed[l][r] = dp[n][m] + (la[l] != la[r]);
}
void gen() {
    int h = id++, st = pos;
    adj[h].clear();
    la[h] = s[++pos];
    while (s[++pos] != ')') adj[h].push_back(id), gen();
    sz[h] = (pos - st) / 3 + 1;
}
void solve() {
    fill(&ed[0][0], &ed[1999][2000], -1);
    scanf("%s", s); id = pos = 0; gen();
    int t = id;
    scanf("%s", s); pos = 0; gen();
    printf("%d\n", f(0, t));
}
int main() {
    for (scanf("%d", &tc); tc--;) solve();
    return 0;
}

13330번: Palindromic

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

동적계획법을 이용해 해결할 수 있다.

s[i]: i번째 문자
lp[i][j]: s[j-i+1 ... j-i+1+k]와 s[j ... j-k]가 같은 최대 k
dp[i]: s[1...i]을 θ-팰린드롬 문자열들을 연결하여 만들 때 필요한 최소 문자열 수
자세한 구현은 다음 소스를 참고하자.


시간복잡도는 $O(n^2)$

#include<cstdio>
#include<algorithm>
using namespace std;
int lp[10001][10001], n, k, l, dp[10001];
char s[10002];
int main() {
    scanf("%d%d%d%s", &n, &k, &l, s + 1);
    for (int i = 2; i <= n; i++) for (int j = i; j <= n; j++) if (s[j - i + 1] == s[j]) lp[i][j] = lp[i - 2][j - 1] + 1;
    for (int i = 1; i <= n; i++) {
        dp[i] = n;
        for (int j = 2; j <= i; j++) if (2 * lp[j][i] * l >= j*k) dp[i] = min(dp[i], dp[i - j] + 1);
    }
    printf("%d", dp[n] < n ? dp[n] : 0);
    return 0;
}

13328번: Message Passing

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

dp[i]: 시각이 i일 때 전화 수
i) i < 0
dp[i] = 0
ii) i = 0
dp[i] = 1
iii) i > 0
dp[i] = dp[i-1] + dp[i-2] + ... + dp[i-d]

답은 dp[t]이다. 행렬을 이용하면 $O(\lg t)$ 횟수의 행렬 곱으로 답을 구할 수 있다.

#include<cstdio>
#define mod 31991
int d, t;
struct st {
    int m[50][50] = {};
    st operator*(st a) const {
        st ret;
        for (int i = 0; i < d; i++) for (int j = 0; j < d; j++) {
            for (int k = 0; k < d; k++) ret.m[i][j] = (ret.m[i][j] + m[i][k] * a.m[k][j]) % mod;
        }
        return ret;
    }
}u, r;
int main() {
    scanf("%d%d", &d, &t);
    for (int i = 0; i < d; i++) r.m[i][i] = u.m[0][i] = 1;
    for (int i = 1; i < d; i++) u.m[i][i - 1] = 1;
    for (; t; t >>= 1, u = u*u) if (t & 1) r = r*u;
    printf("%d", r.m[0][0]);
    return 0;
}

11062번: Card Game

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

c[i]: i번째 카드에 쓰인 수
dp[i][j]: j-i+1, j-i+2, ..., j번째 카드가 남았을 때 선수 플레이어가 만들 수 있는 최대 합
dp[i][j] = sum(c[k]: j-i<k<=j) - min(dp[i-1][j-1],dp[i-1][j])

시간복잡도는 테스트 케이스마다 $O(n^2)$

#include<cstdio>
#include<algorithm>
using namespace std;
int s[1001], n, t;
int main() {
    for (scanf("%d", &t); t--;) {
        int dp[1001] = {};
        scanf("%d", &n);
        for (int i = 1; i <= n; i++) scanf("%d", s + i), s[i] += s[i - 1];
        for (int i = 1; i <= n; i++) for (int j = n; j >= i; j--) dp[j] = s[j] - s[j - i] - min(dp[j - 1], dp[j]);
        printf("%d\n", dp[n]);
    }
    return 0;
}

13162번: Flowey's Love

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

알갱이들이 직선 방향으로 움직일 때 주어진 구역 안에서 돌아다니며 모을 수 있는 최대 알갱이 수를 구하는 문제이다.

대충보면 TSP 이기 때문에 다항시간 안에 풀기 힘들다는 것을 알 수 있다. 주어진 n이 작으므로 모든 상태를 따져보는 DP로 해결한다.
알갱이를 최대한 많이 모으는 방법은 알갱이를 최대한 빠르게 모으는 방법이기도하다. 이에 따라 dp를 다음과 같이 정의한다.
dp[i][j]: 모은 알갱이의 집합이 i이고 영혼이 알갱이 j에 위치해 있을 때 걸리는 최소 시간
dp[i][j] = min(dp[i - j][k] + (dp[i - j][k]시간이 흐른 후 k->j로 가는데 걸리는 최소 시간) : k$\in$i)
* i-j는 집합 i에서 j를 제외함을 의미함.

문제에서는 영혼이 돌아다닐 수 있는 구역이 제한되어 있기 때문에 이를 고려해야 한다. 자세한 구현은 아래 소스를 참고하자.

시간복잡도는 $O(n^2 * 2^n)$

#include<cstdio>
#include<algorithm>
#define eps(u,v) (abs((u)-(v))<1e-9)
#define cmp(u,v) (u<v || eps(u,v))
using namespace std;
int n, xm, ym, res;
double sx[19], sy[19], dp[1 << 19][19], vx[19], vy[19], lt[19];
double f(double x, double y, int h, double t) {
    double xi = sx[h] + vx[h] * t - x, yi = sy[h] + vy[h] * t - y;
    if (eps(xi, 0) && eps(yi, 0)) return 0;
    double a = (xi*vx[h] + yi*vy[h]) * 2, b = xi*xi + yi*yi;
    return eps(a, 0) || cmp(0, b / a) ? 1e5 : -b / a;
}
int main() {
    scanf("%d%d%d", &n, &xm, &ym); n++;
    for (int i = 1, ex, ey; i < n; i++) {
        scanf("%lf%lf%d%d", sx + i, sy + i, &ex, &ey);
        double d = hypot(ex - sx[i], ey - sy[i]);
        vx[i] = (ex - sx[i]) / d;
        vy[i] = (ey - sy[i]) / d;
        lt[i] = max(eps(vx[i], 0) ? 0 : min((-xm - sx[i]) / vx[i], (xm - sx[i]) / vx[i]),
            eps(vy[i], 0) ? 0 : min((-ym - sy[i]) / vy[i], (ym - sy[i]) / vy[i]));
    }
    fill(dp[0], dp[1 << n], 1e5);
    dp[1][0] = 0;
    for (int i = 1; i < 1 << n; i++) {
        int bt = 0;
        for (int j = i; j; j &= j - 1) bt++;
        for (int j = 0; j < n; j++) if (1 << j&i) {
            for (int k = 0; k < n; k++) if (1 << k&i &&j^k) {
                double t = dp[i ^ 1 << j][k], ret = max(t + f(sx[k] + vx[k] * t, sy[k] + vy[k] * t, j, t), lt[j]),
                    tx = sx[j] + vx[j] * ret, ty = sy[j] + vy[j] * ret;
                if (cmp(-xm, tx) && cmp(tx, xm) && cmp(-ym, ty) && cmp(ty, ym)) dp[i][j] = min(dp[i][j], ret);
            }
            if (dp[i][j] < 1e4) res = max(res, bt - 1);
        }
    }
    printf("%d", res);
    return 0;
}

1626번: 두 번째로 작은 스패닝 트리

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

MST를 이루는 간선(e1)을 하나 제거하고 그 간선과 다른 가중치의 간선(e2)으로 forest를 연결해보자. 두 번째로 작은 스패닝 트리는 (e2의 가중치) - (e1의 가중치)가 가장 작을 때 만들어 진다.

e2를 MST에 추가하면 그래프에는 정확히 하나의 사이클이 존재하고 그 사이클에 e1과 e2가 모두 존재한다. 다시 말해 MST에서 e2의 양 끝 정점을 연결하는 MST 위의 경로 중에 e1이 존재한다.

(e2의 가중치) - (e1의 가중치)의 최솟값을 구하기 위해선 MST에 없는 간선(e2)마다 트리 위의 경로 중 가중치가 해당 간선보다 작으면서 가장 큰 간선(e1)을 찾으면 된다.

이러한 문제는 LCA를 구해서 해결할 수 있는 문제로 기본적인 트릭이 잘 알려져 있다. 아래 소스에서는 sparse table을 이용하여 주어진 두 정점에 대해 LCA 및 1, 2번째 최소 가중치 간선을 $O(\lg n)$에 구할 수 있도록 구현했다. 여기서 두 개의 최소 가중치를 구하는 이유는 만약 첫 번째 최소 가중치가 e2의 가중치와 같을 경우 두 번째 가중치를 사용해야 하기 때문이다.

답은 (MST 가중치) + min( (e2의 가중치) - (e1의 가중치) )이다.

최종 시간복잡도는 $O(e\lg v)$

#include<cstdio>
#include<algorithm>
#include<vector>
using namespace std;
struct edge {
    int x, y, d;
}ed[200000];
struct st {
    int f = -1, s = -1;
    st operator+(st t) const {
        st ret = *this;
        if (ret.f^t.f) ret.s = max(ret.s, t.f);
        if (ret.f < ret.s) swap(ret.f, ret.s);
        ret.s = max(ret.s, t.s);
        return ret;
    }
}maxi[50001][16];
int v, e, par[50001], tot, dp[50001][16], lv[50001], ck[200000], cnt, res = -1;
vector<pair<intint> > adj[50001];
int p(int x) { return x^par[x] ? par[x] = p(par[x]) : x; }
void f(int h, int p) {
    for (auto it : adj[h]) if (it.first^p) {
        dp[it.first][0] = h;
        maxi[it.first][0].f = it.second;
        for (int i = 1; i < 16; i++) {
            dp[it.first][i] = dp[dp[it.first][i - 1]][i - 1];
            maxi[it.first][i] = maxi[dp[it.first][i - 1]][i - 1] + maxi[it.first][i - 1];
        }
        lv[it.first] = lv[h] + 1;
        f(it.first, h);
    }
}
st query(int x, int y) {
    st ret;
    if (lv[x] < lv[y]) swap(x, y);
    for (int i = 16; i--;) if (1 << i <= lv[x] - lv[y]) ret = ret + maxi[x][i], x = dp[x][i];
    if (x == y) return ret;
    for (int i = 16; i--;) if (dp[x][i] != dp[y][i]) {
        ret = ret + maxi[x][i] + maxi[y][i];
        x = dp[x][i];
        y = dp[y][i];
    }
    return ret + maxi[x][0] + maxi[y][0];
}
int main() {
    scanf("%d%d", &v, &e);
    for (int i = 0; i < e; i++) scanf("%d%d%d", &ed[i].x, &ed[i].y, &ed[i].d);
    sort(ed, ed + e, [](edge i, edge j) {return i.d < j.d; });
    for (int i = 1; i <= v; i++) par[i] = i;
    for (int i = 0; i < e; i++) {
        int ra = p(ed[i].x), rb = p(ed[i].y);
        if (ra^rb) {
            par[ra] = rb;
            adj[ed[i].x].push_back({ ed[i].y,ed[i].d });
            adj[ed[i].y].push_back({ ed[i].x,ed[i].d });
            tot += ed[i].d;
            ck[i] = 1;
            cnt++;
        }
    }
    if (cnt^v - 1) { puts("-1"); return 0; }
    f(1, 0);
    for (int i = 0; i < e; i++) if (!ck[i]) {
        st ret = query(ed[i].x, ed[i].y);
        if (ret.f^ed[i].d && (!~res || res>ed[i].d - ret.f + tot)) res = ed[i].d - ret.f + tot;
        if (~ret.s && (!~res || res>ed[i].d - ret.s + tot)) res = ed[i].d - ret.s + tot;
    }
    printf("%d", res);
    return 0;
}

1103번: 게임

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

DAG로 가정해서 DP로 푸는 동시에 사이클이 존재하는지 확인한다.

시간복잡도는 $O(nm)$

#include<cstdio>
#include<algorithm>
using namespace std;
const int dx[] = { 0,1,0,-1 }, dy[] = { 1,0,-1,0 };
int n, m, dp[50][50], vis[50][50], p[50][50];
char s[50][51];
int f(int x, int y) {
    if (x < 0 || y < 0 || x >= n || y >= m || s[x][y] == 'H'return 0;
    if (p[x][y]) { puts("-1"); exit(0); }
    int &ret = dp[x][y];
    if (vis[x][y]) return ret;
    p[x][y] = vis[x][y] = 1;
    int t = s[x][y] - '0';
    for (int i = 0; i < 4; i++) ret = max(ret, f(x + t*dx[i], y + t*dy[i]));
    p[x][y] = 0;
    return ++ret;
}
int main() {
    scanf("%d%d", &n, &m);
    for (int i = 0; i < n; i++) scanf("%s", s[i]);
    printf("%d", f(0, 0));
    return 0;
}

13941번: Kronican

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

풀이는 http://hsin.hr/coci/contest3_solutions.zip 3번 참조

시간복잡도는 $O(n^2*2^n)$

#include<cstdio>
#include<algorithm>
using namespace std;
int n, k, c[20][20], dp[1 << 20];
int main() {
    scanf("%d%d", &n, &k);
    for (int i = 0; i < n; i++) for (int j = 0; j < n; j++) scanf("%d", c[i] + j);
    for (int i = 1 << n; i--;) {
        dp[i] = 1e9;
        int cnt = 0;
        for (int x = 0; x < n; x++) if (!(1 << x&i)) {
            for (int y = 0; y < n; y++) if (!(1 << y&i) && x^y) dp[i] = min(dp[i], dp[i | 1 << x] + c[x][y]);
            cnt++;
        }
        if (cnt <= k) dp[i] = 0;
    }
    printf("%d", dp[0]);
    return 0;
}

14553번: The Way

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

시간복잡도는 $O(n)$

#include<cstdio>
#define mod int(1e9 + 9)
int n;
long long dp[1001][3] = { { 1,0,0 },{ 1,1,1 } };
int main() {
    scanf("%d", &n);
    for (int i = 2; i <= n; i++) {
        dp[i][0] = (dp[i - 1][0] * 2 + dp[i - 1][1] + dp[i - 1][2] - dp[i - 2][0] - dp[i - 2][1] + mod * 2) % mod;
        dp[i][1] = (dp[i - 1][0] + dp[i - 1][1] + dp[i - 1][2]) % mod;
        dp[i][2] = (dp[i - 1][0] + dp[i - 1][1] + dp[i - 1][2] * 2 - dp[i - 2][1] - dp[i - 2][2] + mod * 2) % mod;
    }
    printf("%lld", dp[n][2]);
    return 0;
}

7984번: Rabbits

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


#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
int n, k, a[2001], dp[2001][2001], s, res;
int main() {
    scanf("%d%d", &n, &k);
    for (int i = 1; i <= n; i++) scanf("%d", a + i), s += a[i];
    if (2 * k >= n) {
        printf("%d", s);
        return 0;
    }
    for (int i = 1; i <= k; i++) {
        int maxi = 0;
        for (int j = i; j <= n; j++) {
            maxi = max(maxi, dp[i - 1][j - 1]);
            dp[i][j] = maxi + a[j];
            if (i > 1 && j > i) dp[i][j] = max(dp[i][j], dp[i - 1][j - 2] + a[j] + a[j - 1]);
        }
    }
    for (int i = k; i <= n; i++) res = max(res, dp[k][i]);
    memset(dp, 0, sizeof(dp));
    dp[1][1] = a[1];
    for (int i = 2; i <= n; i++) dp[1][i] = -2e9;
    for (int i = 2; i <= k; i++) {
        int maxi = 0;
        for (int j = i; j <= n; j++) {
            maxi = max(maxi, dp[i - 1][j - 1]);
            dp[i][j] = maxi + a[j];
            if (i > 1 && j > i) dp[i][j] = max(dp[i][j], dp[i - 1][j - 2] + a[j] + a[j - 1]);
        }
    }
    for (int i = 1; i < k; i++) {
        int maxi = 0;
        for (int j = k - i; j <= n - 2 * i; j++) maxi = max(maxi, dp[k - i][j]);
        for (int j = n - 2 * i + 1; j <= n; j++) maxi += a[j];
        res = max(res, maxi);
    }
    rotate(a + 1, a + 2, a + 1 + n);
    memset(dp, 0, sizeof(dp));
    dp[1][1] = a[1];
    for (int i = 2; i <= n; i++) dp[1][i] = -2e9;
    for (int i = 2; i <= k; i++) {
        int maxi = 0;
        for (int j = i; j <= n; j++) {
            maxi = max(maxi, dp[i - 1][j - 1]);
            dp[i][j] = maxi + a[j];
            if (i > 1 && j > i) dp[i][j] = max(dp[i][j], dp[i - 1][j - 2] + a[j] + a[j - 1]);
        }
    }
    for (int i = 1; i < k; i++) {
        int maxi = 0;
        for (int j = k - i; j <= n - 2 * i; j++) maxi = max(maxi, dp[k - i][j]);
        for (int j = n - 2 * i + 1; j <= n; j++) maxi += a[j];
        res = max(res, maxi);
    }
    printf("%d", res);
    return 0;
}

12010번: Landscaping

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

이 문제를 풀기에 앞서 koi 소방차 문제를 풀어보길 추천한다.

1. 각 화단마다 b[i]-a[i] 값만 중요하다. 이 값이 양수이면 흙을 추가해야 하고 음수면 버려야 한다.
2. b[i]-a[i] 값의 절댓값의 개수만큼 해당 위치 i에 1(추가) 혹은 -1(버림)이 있다고 하자.
3. 그러면 문제에 서술된 방법은 1을 없애거나 -1을 없애거나 1과 -1을 동시에 없애는 연산에 대응된다.
4. 소방차 문제 솔루션과 똑같이 여러 개의 층을 만들어서 1이면 층을 증가시키면서, -1이면 감소시키면서 위치(i)와 증감 여부를 저장한다.
5. 소방차 풀이와 마찬가지로 각 층에서 답을 구해서 합해도 최적임이 보장된다.
6. 같은 층에는 1과 -1이 번갈아 존재한다. 짝을 지어 없앨 때는 인접한 쌍의 1과 -1을 없애기만 해도 된다.
7. 각 층마다의 해를 dp로 풀고 답을 합치자.

시간복잡도는 $O(n)$

#include<cstdio>
#include<vector>
#include<algorithm>
using namespace std;
const int MXN = 1e5;
int n, x, y, z, c, a[MXN], b[MXN];
long long res;
vector<int> v[MXN * 20];
void f(vector<int> &p) {
    vector<long long> dp(p.size() + 1);
    for (int i = 0; i < p.size(); i++) {
        dp[i + 1] = dp[i] + (a[p[i]] < b[p[i]] ? x : y);
        if (i) dp[i + 1] = min(dp[i + 1], dp[i - 1] + (p[i] - p[i - 1])*z);
    }
    res += dp.back();
}
int main() {
    scanf("%d%d%d%d", &n, &x, &y, &z);
    c = 10 * n;
    for (int i = 0; i < n; i++) {
        scanf("%d%d", a + i, b + i);
        for (int j = a[i]; j < b[i]; j++) v[c++].push_back(i);
        for (int j = a[i]; j > b[i]; j--) v[--c].push_back(i);
    }
    for (int i = 0; i < 20 * n; i++) f(v[i]);
    printf("%lld", res);
    return 0;
}

9521번: PALETA

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

fi <- i를 모두 연결해보면 트리나 하나의 사이클에 트리가 달린 그래프만 존재한다.
만약 모든 사이클에 채색을 마쳤다면 나머지 노드들에게는 각각 k-1가지의 채색 방법이 존재한다.
따라서 답은 각 사이클 채색 수 곱 * (k-1)^나머지 노드 수이다.
사이클 채색은 위키피디아 참조
https://en.wikipedia.org/wiki/Graph_coloring#Chromatic_polynomial

아래 소스의 시간복잡도는 $O(n)$

#include<cstdio>
#define mod (int(1e9)+7)
int n, k, a[1000001], vis[1000001], s, c;
long long r = 1, dp[1000001];
int main() {
    scanf("%d%d", &n, &k);
    for (int i = 1; i <= n; i++) scanf("%d", a + i);
    dp[0] = k;
    for (int i = 2; i <= n; i++) dp[i] = (dp[i - 1] * (k - 2) + dp[i - 2] * (k - 1)) % mod;
    dp[1] = k;
    for (int i = 1; i <= n; i++) if (!vis[i]) {
        int j = i;
        while (!vis[j]) vis[j] = ++c, j = a[j];
        if (vis[j] >= vis[i]) r = r*dp[c - vis[j] + 1] % mod, s += c - vis[j] + 1;
    }
    for (int i = n - s; i--;) r = r*(k - 1) % mod;
    printf("%lld", r);
    return 0;
}

5982번: Forgotten Password

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

dp[i]: 0..i번째 글자를 정했을 때, 사전 순으로 가장 빠른 단어


아래 소스의 시간복잡도는 $O(l^2+l*nw*L_w)$ // $L_w$은 사전 내 단어의 최대 길이

#include<iostream>
#include<string>
using namespace std;
string w[1000], r[1001], p;
int l, nw;
int main() {
    cin >> l >> nw >> p;
    for (int i = 0; i < nw; i++) cin >> w[i];
    for (int i = 0; i < l; i++) if (!i || r[i] != "") {
        for (int j = 0; j < nw; j++) if (i + w[j].length() <= l) {
            int k = 0;
            for (; k < w[j].length(); k++) if (p[i + k] ^ '?' && p[i + k] ^ w[j][k]) break;
            if (k == w[j].length() && (r[i + k] == "" || r[i + k] > r[i] + w[j])) r[i + k] = r[i] + w[j];
        }
    }
    cout << r[l];
    return 0;
}

1693번: 트리 색칠하기

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

$O(n\lg n)$

n개의 노드를 색칠하는데 대략 $\log_2 n$ 종류의 색밖에 필요없다.
이를 증명해보자.
$a_i$: 각 색마다 색칠하는 비용이 다를 때, 임의의 그래프에서 i가지 색칠로 최적해를 구하는데 지장이 없는 최대 노드 개수
i번째로 비용이 작은 색을 칠한 노드는 1..i-1번째로 비용이 작은 색을 칠한 노드와 연결되어 있다.
고로, $a_i >= a_1 + a_2 + ... + a_{i-1} + 1$
수학적 귀납법을 통해 $a_i$의 최솟값은 $2^{i-1}$임을 알 수 있다.

이 성질을 이용하여 주어진 그래프를 1번 노드를 root로 하는 rooted tree를 만든 후 dp를 한다.
dp[i][j]: j로 색칠된 i번 노드를 root로 하는 서브트리를 색칠하는데 드는 최소 비용
dp[i][j] = j + sum(k는 노드 i의 자식)(min(l != j)(dp[k][l]))

답은 min(i)(dp[1][i])

#include<cstdio>
#include<vector>
#include<algorithm>
using namespace std;
int dp[100001][18], n;
vector<int> adj[100001];
void f(int h, int p) {
    dp[h][0] = 1e9;
    for (int i = 1; i < 18; i++) dp[h][i] += i;
    for (int it : adj[h]) if (it^p) {
        f(it, h);
        int f = 0, s = 0;
        for (int j = 1; j < 18; j++) {
            if (dp[it][s] > dp[it][j]) s = j;
            if (dp[it][f] > dp[it][s]) swap(f, s);
        }
        for (int j = 1; j < 18; j++) dp[h][j] += dp[it][f^j ? f : s];
    }
}
int main() {
    scanf("%d", &n);
    for (int i = 1, x, y; i < n; i++) {
        scanf("%d%d", &x, &y);
        adj[x].push_back(y);
        adj[y].push_back(x);
    }
    f(1, 0);
    printf("%d", *min_element(dp[1] + 1, dp[1] + 18));
    return 0;
}

11056번: 두 부분 문자열

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

$O(l_al_b)$

답은 (a 길이) + (b 길이) - (LCA(a,b) 길이)

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
char a[1002], b[1002];
int dp[1001][1001], n, m;
int main() {
    scanf("%s%s", a + 1, b + 1);
    n = strlen(a + 1);
    m = strlen(b + 1);
    for (int i = 1; a[i]; i++)
        for (int j = 1; b[j]; j++)
            dp[i][j] = max({ dp[i - 1][j - 1] + (a[i] == b[j]),dp[i - 1][j],dp[i][j - 1] });
    printf("%d", n + m - dp[n][m]);
    return 0;
}

1736번: 쓰레기 치우기

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

$O(nm)$

다음 문제를 DP로 푼다.

1. 가장 오른쪽 위에서 왼쪽 혹은 아래로 이동한다.
2. 같은 행, 열에서는 최대 하나의 쓰레기를 수거할 수 있다.
위 조건을 만족하며 수거 가능한 쓰레기 수의 최댓값을 구하여라.

#include<cstdio>
#include<algorithm>
using namespace std;
int dp[101][101], n, m;
int main() {
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; i++) {
        for (int j = m; j; j--) scanf("%d", dp[i] + j);
        for (int j = 1; j <= m; j++) dp[i][j] = max({ dp[i - 1][j],dp[i][j - 1],dp[i - 1][j - 1] + dp[i][j] });
    }
    printf("%d", dp[n][m]);
    return 0;
}

1785번: 보드 게임

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


$O((\log n)^4)$

주어진 방법으로 지폐단위를 만들면 큰 지폐 단위부터 순서대로 최대한 환전했을 때 필요한 지폐 총 개수가 최소가 된다.
이러한 특성을 이용하여 다음 점화식을 통해 DP를 한다.

dp[i][j][k][l]: 2,3,4,5를 각각 i,j,k,l번 곱해 나온 지폐 단위 + 이후 만든 더 큰 지폐 단위를 이용하여 나머지를 최소화하며 환전할 때 필요한 최소 지폐 수
p = 2^i * 3^j * 4^k * 5^l이면
dp[i][j][k][l] = min(
dp[i+1][j][k][l] + n/p%2,
dp[i][j+1][k][l] + n/p%3,
dp[i][j][k+1][l] + n/p%4,
dp[i][j][k][l+1] + n/p%5)

답은 dp[0][0][0][0]


#include<cstdio>
#include<algorithm>
using namespace std;
long long n, dp[60][38][30][26];
int k, c[6];
long long f(long long x, int y) {
    if (!x || !y) return x;
    long long &ret = dp[c[2]][c[3]][c[4]][c[5]];
    if (ret) return ret;
    ret = 1e18;
    for (int i = 2; i <= 5; i++) {
        c[i]++;
        ret = min(ret, x%i + f(x / i, y - 1));
        c[i]--;
    }
    return ret;
}
int main() {
    scanf("%lld %d", &n, &k);
    printf("%lld", f(n, k - 1));
    return 0;
}

2237번: 수열 축소

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

$O(nT)$ // T: t가 가능한 구간 크기

축소 연산을 통해 A[1] - A[2]에 나머지 항들(A[3..n])을 더하거나 빼서 나오는 수를 모두 만들 수 있다.
이를 수식으로 쓰고 A[2..n] 앞에 있는 부호를 나열해보면 1개의 -와 뒤이은 0개 이상의 +들 묶음들로 이루어졌다는 것을 알 수 있다.
앞에서 부터 각 묶음에 대해 +가 x개이면 순서대로 i = 2를 x번, i = 1을 1번 축소 연산을 수행하면 된다.

#include<cstdio>
int n, t, a[101], dp[101][20001], r[101];
int main() {
    scanf("%d%d", &n, &t);
    for (int i = 1; i <= n; i++) scanf("%d", a + i);
    dp[2][a[1] - a[2] + 10000] = 1;
    for (int i = 3; i <= n; i++)
        for (int j = 0; j < 20001; j++)
            if (dp[i - 1][j]) dp[i][j - a[i]] = 1, dp[i][j + a[i]] = 2;
    for (int i = n, p = t + 10000; i > 1; i--) {
        r[i] = dp[i][p];
        p = r[i] - 1 ? p - a[i] : p + a[i];
    }
    for (int i = 3; i <= n; i++) printf("%d\n", r[i]);
    puts("1");
    return 0;
}

1836번: 트리의 가짓수 세기

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

$O(nk)$

dp[i][j]: 높이가 i이면서 노드 수가 j인 트리의 개수
$dp[i][j] = \sum_{k=0}^{j-1}(2*\sum_{l=1}^{i-2}dp[l][k] + dp[i-1][k])*dp[i-1][j-1-k]$
$\sum_{l=1}^{i-2}dp[l][k]$ 계산은 누적합을 이용해 빠르게 구할 수 있다.

#include<cstdio>
#define mod 9901
int dp[100][200], s[200], n, m;
int main() {
    scanf("%d%d", &n, &m);
    dp[1][1] = 1;
    for (int i = 2; i <= m; i++) {
        for (int j = 1; j <= n; j++) {
            s[j] = (s[j] + dp[i - 2][j]) % mod;
            for (int k = 0; k < j; k++)
                dp[i][j] = (dp[i][j] + (2 * s[k] + dp[i - 1][k])*dp[i - 1][j - 1 - k]) % mod;
        }
    }
    printf("%d", dp[m][n]);
    return 0;
}

1462번: 퀴즈쇼

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


$O(n)$

a[i]: i번 문제 점수
b[i]: i번 문제 보너스 점수
s[i]: sum(a[1..i])
dp[i][j]: 1..i번 문제를 풀었고 j개의 쿠폰을 가지고 있을 때 최대 점수
dp[i][0] = max(max(dp[i-1][0..m-1] - a[i]), dp[i-1][m-1] + a[i] + b[i])
dp[i][j](j>0) = dp[i-1][j-1] + a[i]

다음과 같이 p[i], q[i]를 정의하자.
p[i] = max(dp[i][0..m-1])
q[i] = dp[i][0]

dp[i-1][m-1] + a[i] + b[i] = dp[i-m][0] + s[i] - s[i-m] + b[i] 이므로
q[i] = max(p[i-1] - a[i], q[i-m] + s[i] - s[i-m] + b[i])  ...  (*)
또한,
max(p[i-1] + a[i], q[i])
= max(max(dp[i-1][0..m-1]+a[i]), q[i])
= max(dp[i][1..m-1], max(dp[i-1][m-1]+a[i], q[i]))
= max(dp[i][1..m-1], q[i])  ...  (*)에 의해
= p[i]

정리하면
p[i] = max(p[i-1] + a[i], q[i])
q[i] = max(p[i-1] - a[i], q[i-m] + s[i] - s[i-m] + b[i])

답은 p[n]


#include<cstdio>
#include<algorithm>
using namespace std;
int n, m, a[500001];
long long s[500001], q[500001], p;
int main() {
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; i++) scanf("%d", a + i), s[i] = s[i - 1] + a[i];
    for (int i = 1, b; i <= n; i++) {
        scanf("%d", &b);
        q[i] = i < m ? p - a[i] : max(p - a[i], q[i - m] + s[i] - s[i - m] + b);
        p = max(p + a[i], q[i]);
    }
    printf("%lld", p);
    return 0;
}