《挑战程序设计竞赛》课后练习题解集——3.4 熟练掌握动态规划
Posted hs-zlq
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了《挑战程序设计竞赛》课后练习题解集——3.4 熟练掌握动态规划相关的知识,希望对你有一定的参考价值。
《挑战程序设计竞赛》课后练习题解集——3.4 熟练掌握动态规划
状态压缩DP
POJ 2441 有N头牛,M个槽,N,M≤20,每头牛只在指定的pi个槽里进食,不与其他牛共享槽。问有多少种分配方案。
dp[i][S],当前第i头牛要进食,槽的使用状态为S
1 #include <cstdio>
2 #include <cstring>
3 #include <vector>
4 using namespace std;
5
6 int n, m;
7 int dp[1 << 21];
8 vector<int> cow[21];
9
10 int next(int k) {
11 int x = k & (-k), y = x + k;
12 return k = ((k & (~y)) / x >> 1) | y;
13 }
14
15 int main() {
16 int p, x;
17 scanf("%d%d", &n, &m);
18 for (int i = 0; i < n; i++) {
19 scanf("%d", &p);
20 for (int j = 0; j < p; j++) {
21 scanf("%d", &x);
22 x--;
23 cow[i].push_back(x);
24 }
25 }
26 dp[0] = 1;
27 for (int i = 0; i < n; i++) {
28 for (int j = (1 << i + 1) - 1; j < 1 << m; j = next(j)) {
29 for (int k = 0; k < cow[i].size(); k++) {
30 if (j >> cow[i][k] & 1) {
31 dp[j] += dp[j & ~(1 << cow[i][k])];
32 }
33 }
34 }
35 }
36 int res = 0;
37 for (int i = (1 << n) - 1; i < 1 << m; i = next(i)) {
38 res += dp[i];
39 }
40 printf("%d", res);
41 }
POJ 3254 M×N的农场,N, M≤12,农夫要选择若干个肥沃的地方种种子,这些种子不能相邻,问有多少种方案。
和书上的铺砖问题做法类似,但是要注意此时used数组的含义不是已有种子,而是(i-1, j)有种子,所以(i, j+1)used为true时,(i, j)仍然可以播种;另外初始化的时候,可以全为1或者有意义的情况为1,因为对于(m-1, n-1) dp数组是考虑最后一行的放法,原先的铺砖问题砖一定不会铺到下一行,所有初始化只需dp[0]=1。
1 #include <cstdio>
2 #include <cstring>
3 #include <iostream>
4 using namespace std;
5 const int mod = 1e8;
6
7 int m, n, cnt, a[13][13];
8 int dp[2][(1 << 12) + 5];
9
10 int main() {
11 scanf("%d %d", &m, &n);
12 for (int i = 0; i < m; i++) {
13 for (int j = 0; j < n; j++) scanf("%d", &a[i][j]);
14 }
15 int *crt = dp[0], *next = dp[1];
16
17 /* int last = 0;
18 for (int i = n - 1; i >= 0; i--) {
19 last <<= 1;
20 last += a[m - 1][i] == 0;
21 }*/
22 for (int i = 0; i < 1 << n; i++) {
23 /* if (!(last & i)) */ crt[i] = 1;
24 }
25
26 for (int i = m - 1; i >= 0; i--) {
27 for (int j = n - 1; j >= 0; j--) {
28 for (int s = 0; s < 1 << n; s++) {
29 if (a[i][j] == 0 || s >> j & 1)
30 next[s] = crt[s & ~(1 << j)];
31 else {
32 int t = s | 1 << j;
33 if (j < n - 1) t |= 1 << j + 1;
34 next[s] = (crt[s] + crt[t]) % mod;
35 }
36 }
37 swap(crt, next);
38 }
39 }
40 printf("%d
", crt[0]);
41 }
另外,还可以预处理不相邻的种子的放法,按行DP,枚举上下两行的情况来转移
1 #include <cstdio>
2 #include <cstring>
3 #include <iostream>
4 using namespace std;
5 const int mod = 1e8;
6
7 int m, n, cnt, a[13][13], bad[13];
8 int sta[400], dp[2][400];
9
10 int main() {
11 scanf("%d%d", &m, &n);
12 for (int i = 0; i < m; i++) {
13 for (int j = 0; j < n; j++) scanf("%d", &a[i][j]);
14 for (int j = n - 1; j >= 0; j--) {
15 bad[i] <<= 1;
16 bad[i] += !a[i][j];
17 }
18 }
19 for (int i = 0; i < 1 << n; i++) {
20 if (!((i << 1) & i)) sta[cnt++] = i;
21 }
22 int *crt = dp[0], *next = dp[1];
23 for (int i = 0; i < cnt; i++) {
24 if (!(sta[i] & bad[0])) crt[i] = 1;
25 }
26 for (int i = 1; i < m; i++) {
27 for (int j = 0; j < cnt; j++) { //当前行的放置方式
28 if (sta[j] & bad[i]) continue;
29 for (int k = 0; k < cnt; k++) { //上一行的放置方式
30 if (sta[j] & sta[k]) continue;
31 next[j] = (next[j] + crt[k]) % mod;
32 }
33 }
34 swap(crt, next);
35 for (int i = 0; i < cnt; i++) next[i] = 0;
36 }
37 int res = 0;
38 for (int i = 0; i < cnt; i++) res = (res + crt[i]) % mod;
39 printf("%d", res);
40 }
POJ 2836 平面上有N(≤15)个点,要用若干个矩形覆盖这些点,每个矩形至少覆盖到两个点且面积大于0,问这些矩形的面积总和的最小值。
任何方案都可以分解成若干个这样的矩形,它的对角一定有2个点。枚举N^2这样的矩形作为转移边,以覆盖哪些点为状态,就可以转移了
1 #include <cstdio>
2 #include <cstring>
3 #include <iostream>
4 using namespace std;
5 const int INF = 0x7f7f7f7f;
6
7 int n, cnt, x[16], y[16];
8 int area[270], cover[270], dp[1 << 16];
9
10 int main() {
11 while (~scanf("%d", &n) && n) {
12 for (int i = 0; i < n; i++) {
13 scanf("%d", &x[i]);
14 scanf("%d", &y[i]);
15 }
16 cnt = 0;
17 memset(cover, 0, sizeof(cover));
18 memset(dp, 127, sizeof(dp));
19 for (int i = 0; i < n; i++) {
20 for (int j = i + 1; j < n; j++) {
21 int x1 = min(x[i], x[j]), x2 = max(x[i], x[j]),
22 y1 = min(y[i], y[j]), y2 = max(y[i], y[j]);
23 area[cnt] = (x1 == x2 ? 1 : x2 - x1) * (y1 == y2 ? 1 : y2 - y1);
24 for (int k = 0; k < n; k++) {
25 if (x1 <= x[k] && x[k] <= x2 && y1 <= y[k] && y[k] <= y2) {
26 cover[cnt] += (1 << k);
27 }
28 }
29 cnt++;
30 }
31 }
32 dp[0] = 0;
33 for (int i = 0; i < 1 << n; i++) {
34 if (dp[i] != INF) {
35 for (int j = 0; j < cnt; j++) {
36 dp[i | cover[j]] = min(dp[i | cover[j]], dp[i] + area[j]);
37 }
38 }
39 }
40 printf("%d
", dp[(1 << n) - 1]);
41 }
42 }
POJ 1795 给出N个串Si, N≤15, Si≤100,问最短且字典序最小的主串,使得这N个串都是它的子串。
预处理N个串间互为字串的情况,剩下要决定的就是连接顺序。dp[i][S],代表包含的串集合为S,开头是串i,之所以以开头而不是结尾为是为了后面要求字典序最小方便。(我自己的这个版本代码写麻烦了,dp的时候就储存串的连接顺序要好些;而且在以长度回溯串时,如果没有预处理起始串互为字串,只比较前缀其实是错的)
1 #include <cstdio>
2 #include <cstring>
3 #include <iostream>
4 using namespace std;
5 const int INF = 0x7f7f7f7f;
6
7 int n, cnt, sta[16];
8 string str[16];
9 int cost[16][16], dp[16][(1 << 15) + 5];
10
11 int main() {
12 int T;
13 scanf("%d", &T);
14 for (int I = 1; I <= T; I++) {
15 memset(sta, 0, sizeof(sta));
16 memset(cost, 0, sizeof(cost));
17 memset(dp, 127, sizeof(dp));
18 cnt = 0;
19 scanf("%d", &n);
20 for (int i = 0; i < n; i++) cin >> str[i];
21 for (int i = 0; i < n; i++) {
22 for (int j = i + 1; j < n; j++) {
23 if (str[i].find(str[j]) != string::npos)
24 sta[j] = 1;
25 else if (str[j].find(sta[i]) != string::npos)
26 sta[i] = 1;
27 }
28 }
29 for (int i = 0; i < n; i++)
30 if (!sta[i]) str[cnt++] = str[i];
31 for (int i = 0; i < cnt; i++) {
32 for (int j = 0; j < cnt; j++) {
33 if (i != j) {
34 int len = min(str[i].size(), str[j].size());
35 for (int k = 0; k <= len; k++) {
36 if (str[i].substr(str[i].size() - k) ==
37 str[j].substr(0, k)) {
38 cost[i][j] = str[i].size() - k;
39 //在j前面接i要花cost[i][j]的代价
40 }
41 }
42 }
43 }
44 }
45 for (int i = 0; i < cnt; i++) dp[i][1 << i] = str[i].size();
46 for (int i = 1; i < 1 << cnt; i++) {
47 for (int j = 0; j < cnt; j++) {
48 if (dp[j][i] != INF) {
49 for (int k = 0; k < cnt; k++) {
50 if (!(i >> k & 1)) {
51 dp[k][i | (1 << k)] =
52 min(dp[k][i | (1 << k)], cost[k][j] + dp[j][i]);
53 }
54 }
55 }
56 }
57 }
58 int id = 0, sz = (1 << cnt) - 1;
59 for (int i = 1; i < cnt; i++) {
60 if (dp[i][sz] < dp[id][sz] ||
61 dp[i][sz] == dp[id][sz] && str[i] < str[id]) {
62 id = i;
63 }
64 }
65 string ans = str[id];
66 int S = sz;
67 for (int i = 1; i < cnt; i++) {
68 int next_id = -1;
69 string tmp = "";
70 for (int j = 0; j < cnt; j++) {
71 if (id != j && (S >> j & 1) &&
72 dp[id][S] == dp[j][S & ~(1 << id)] + cost[id][j]) {
73 string t = str[j].substr(str[id].size() - cost[id][j]);
74 if (next_id == -1 || t < tmp) {
75 tmp = t;
76 next_id = j;
77 }
78 }
79 }
80 ans += tmp;
81 S &= ~(1 << id);
82 id = next_id;
83 }
84 printf("Scenario #%d:
", I);
85 cout << ans << "
";
86 }
87 }
POJ 3411 给出N个点,M条边,N, M≤10,每条边有ai, bi, ci, Pi, Ri,Pi≤Ri,表示从点ai到bi时,若已经过ci,代价为Pi,否则为Ri。问从点1到点N的最小代价
和旅行商问题类似,dp[i][S]表示当前经过的点集合为S,在点 i 的最小代价。但是此时状态之间不是拓扑序,可能出现回到已经经过过的点的情况,所以要用最段路算法。
1 #include <cstdio>
2 #include <cstring>
3 #include <iostream>
4 #include <queue>
5 #include <vector>
6 using namespace std;
7 #define P pair<int, int>
8 const int INF = 0x7f7f7f7f;
9
10 int n, m;
11 int dp[11][(1 << 10) + 5];
12
13 struct edge {
14 int to, c, p, r;
15 };
16 vector<edge> G[11];
17
18 struct node {
19 int cost, u, S;
20 bool operator<(const node& o) const { return cost < o.cost; }
21 };
22 priority_queue<node> que;
23
24 int main() {
25 int a, b, c, p, r;
26 scanf("%d %d", &n, &m);
27 for (int i = 0; i < m; i++) {
28 scanf("%d %d %d %d %d", &a, &b, &c, &p, &r);
29 a--, b--, c--;
30 edge x = {b, c, p, r};
31 G[a].push_back(x);
32 }
33 memset(dp, 127, sizeof(dp));
34 dp[0][1] = 0;
35 node o = {0, 0, 1};
36 que.push(o);
37 while (!que.empty()) {
38 node p = que.top();
39 que.pop();
40 int u = p.u, rou = p.S;
41 for (int i = 0; i < G[u].size(); i++) {
42 edge es = G[u][i];
43 int v = es.to, cost;
44 if (rou >> es.c & 1)
45 cost = es.p;
46 else
47 cost = es.r;
48 if (dp[v][rou | (1 << v)] > dp[u][rou] + cost) {
49 dp[v][rou | (1 << v)] = dp[u][rou] + cost;
50 o = {dp[v][rou | (1 << v)], v, rou | 1 << v};
51 que.push(o);
52 }
53 }
54 }
55 int res = INF;
56 for (int i = 0; i < 1 << n; i++) res = min(res, dp[n - 1][i]);
57 if (res != INF)
58 printf("%d
", res);
59 else
60 printf("impossible
");
61 }
另外不知道为什么用队列代替优先队列也能0ms过。dij用队列代替优先队列的算法复杂度有待分析
1 #include <cstdio>
2 #include <cstring>
3 #include <iostream>
4 #include <queue>
5 #include <vector>
6 using namespace std;
7 #define P pair<int, int>
8 const int INF = 0x7f7f7f7f;
9
10 int n, m;
11 int dp[11][(1 << 10) + 5];
12
13 struct edge {
14 int to, c, p, r;
15 };
16 vector<edge> G[11];
17
18 queue<P> que;
19
20 int main() {
21 int a, b, c, p, r;
22 scanf("%d %d", &n, &m);
23 for (int i = 0; i < m; i++) {
24 scanf("%d %d %d %d %d", &a, &b, &c, &p, &r);
25 a--, b--, c--;
26 edge x = {b, c, p, r};
27 G[a].push_back(x);
28 }
29 memset(dp, 127, sizeof(dp));
30 dp[0][1] = 0;
31 que.push(P(0, 1));
32 while (!que.empty()) {
33 P p = que.front();
34 que.pop();
35 int u = p.first, rou = p.second;
36 for (int i = 0; i < G[u].size(); i++) {
37 edge es = G[u][i];
38 int v = es.to, cost;
39 if (rou >> es.c & 1)
40 cost = es.p;
41 else
42 cost = es.r;
43 if (dp[v][rou | (1 << v)] > dp[u][rou] + cost) {
44 dp[v][rou | (1 << v)] = dp[u][rou] + cost;
45 que.push(P(v, rou | (1 << v)));
46 }
47 }
48 }
49 int res = INF;
50 for (int i = 0; i < 1 << n; i++) res = min(res, dp[n - 1][i]);
51 if (res != INF)
52 printf("%d
", res);
53 else
54 printf("impossible
");
55 }
矩阵的幂
POJ 3420 问用1×2的矩阵填充4×N的矩阵有多少种方法。
设题述答案为An。考虑这样一类的填充方案,它在前N-1列的空隙中,都至少有一个横放的矩形穿过了这个空隙,设这样的方案有Bn种。我们有B1=1, B2 = 4, Bi = 2 (i为大于2的奇数),Bi = 3(i为大于2的偶数)。对于一般的填充方案,考虑从左到右(右到左是一样)第一列不满足有横放的矩阵穿过空隙的情况(也可能不存在,属于Bi的范畴),然后以此为界限就可以把整个矩形视作两部分,让左右方案数相乘。得到递推关系 An = B1 × An-1 + B2 × An-2 + ... + Bn = An-1 + 4An-2 + 2An-3 + 3An-4 + ...。我们发现后面系数是以“2 3 2 3”循环的,利用这个可以消去后面的许多项,最后得到An = An-1 + 5×An-2 + An-3 - An-4。
1 #include <cstdio>
2 #include <iostream>
3 #include <vector>
4 using namespace std;
5 #define ll long long
6 typedef vector<long long> vec;
7 typedef vector<vec> mat;
8
9 int n, m;
10
11 mat mul(mat A, mat B) {
12 mat C(4, vec(4, 0));
13 for (int i = 0; i < 4; i++) {
14 for (int j = 0; j < 4; j++) {
15 for (int k = 0; k < 4; k++) {
16 C[i][j] = ((C[i][j] + A[i][k] * B[k][j]) % m + m) % m;
17 }
18 }
19 }
20 return C;
21 }
22
23 mat ksm(mat A, int x) {
24 mat B(4, vec(4, 0));
25 for (int i = 0; i < 4; i++) B[i][i] = 1;
26 while (x) {
27 if (x & 1) B = mul(B, A);
28 A = mul(A, A);
29 x >>= 1;
30 }
31 return B;
32 }
33
34 void solve() {
35 mat A(4, vec(4));
36 A[0][0] = 1, A[0][1] = 5, A[0][2] = 1, A[0][3] = -1;
37 A[1][0] = A[2][1] = A[3][2] = 1;
38 A = ksm(A, n - 1);
39 printf("%lld
", (A[3][0] * 36 + A[3][1] * 11 + A[3][2] * 5 + A[3][3]) % m);
40 }
41
42 int main() {
43 while (~scanf("%d%d", &n, &m), n && m) {
44 solve();
45 }
46 }
POJ 3735 有n只小猫,要依次执行k个操作并重复m次,n≤100, k≤100, ≤1e9,操作分别有:1. 使第i只小猫获得一个坚果;2. 使第i只小猫吃掉它的所有坚果;3. 交换第i只小猫与第j只小猫的坚果。问最后每只小猫拥有坚果的情况。
小猫的坚果数和常数1写作列向量,3种操作都对应到矩阵运算即可。这题的矩阵相乘需要判0优化,否则会T。
1 #include <cstdio>
2 #include <iostream>
3 #include <vector>
4 using namespace std;
5 #define ll long long
6 typedef vector<ll> vec;
7 typedef vector<vec> mat;
8
9 int n, m, k;
10
11 mat mul(mat &A, mat &B) {
12 mat C(A.size(), vec(B[0].size()));
13 for (int i = 0; i < A.size(); i++) {
14 for (int j = 0; j < A[0].size(); j++) {
15 if (A[i][j])
16 for (int k = 0; k < B[0].size(); k++) {
17 C[i][k] += A[i][j] * B[j][k];
18 }
19 }
20 }
21 return C;
22 }
23
24 mat ksm(mat A, int x) {
25 mat B(A.size(), vec(A.size()));
26 for (int i = 0; i < A.size(); i++) B[i][i] = 1;
27 while (x) {
28 if (x & 1) B = mul(B, A);
29 A = mul(A, A);
30 x >>= 1;
31 }
32 return B;
33 }
34
35 int main() {
36 char op;
37 int x, y;
38 while (~scanf("%d%d%d", &n, &m, &k), n || m || k) {
39 mat a(n + 1, vec(n + 1));
40 for (int i = 0; i < n + 1; i++) a[i][i] = 1;
41 for (int i = 0; i < k; i++) {
42 getchar();
43 scanf("%c", &op);
44 if (op == ‘g‘) {
45 scanf("%d", &x);
46 for (int i = 0; i < n + 1; i++) a[x - 1][i] += a[n][i];
47 } else if (op == ‘e‘) {
48 scanf("%d", &x);
49 for (int i = 0; i < n + 1; i++) a[x - 1][i] = 0;
50 } else {
51 scanf("%d%d", &x, &y);
52 for (int i = 0; i < n + 1; i++) swap(a[x - 1][i], a[y - 1][i]);
53 }
54 }
55 a = ksm(a, m);
56 for (int i = 0; i < n; i++)
57 printf("%lld%c", a[i][n], i == n - 1 ? ‘
‘ : ‘ ‘);
58 }
59 }
利用数据结构高效求解
POJ 3171 有N只牛,每只牛可以在[T1, T2]的时间里工作,需要支付它S薪水。现需要[M, E]的时间区间里每一时刻都有牛在工作,问最少支付的薪水是多少
dp[i]表示到第i时刻都有牛工作最少需要支付的薪水是多少,线段树维护。
1 #include <algorithm>
2 #include <cstdio>
3 #include <iostream>
4 using namespace std;
5 #define ll long long
6 const int N = 1e4 + 5;
7 const int M = 86399 + 5;
8 const ll INF = 1e10;
9
10 int n, m, e;
11
12 ll min(ll a, ll b) { return a < b ? a : b; }
13
14 struct sec {
15 int l, r, s;
16 } a[N];
17
18 bool operator<(sec x, sec y) { return x.r < y.r; }
19
20 struct node {
21 int l, r, f;
22 ll Min;
23 } tree[M * 4];
24
25 void build(int k, int lch, int rch) {
26 tree[k] = {lch, rch, 0, INF};
27 if (lch == rch) return;
28 int mid = (lch + rch) / 2;
29 build(k * 2, lch, mid);
30 build(k * 2 + 1, mid + 1, rch);
31 }
32
33 void down(int k) {
34 if (tree[k * 2].Min > tree[k].Min) {
35 tree[k * 2].Min = tree[k].Min;
36 tree[k * 2].f = 1;
37 }
38 if (tree[k * 2 + 1].Min > tree[k].Min) {
39 tree[k * 2 + 1].Min = tree[k].Min;
40 tree[k * 2 + 1].f = 1;
41 }
42 tree[k].f = 0;
43 }
44
45 ll ask(int k, int x) {
46 if (tree[k].l == tree[k].r) return tree[k].Min;
47 if (tree[k].f) down(k);
48 int mid = (tree[k].l + tree[k].r) / 2;
49 if (x <= mid) return ask(k * 2, x);
50 return ask(k * 2 + 1, x);
51 }
52
53 void update(int k, int lch, int rch, ll num) {
54 if (lch <= tree[k].l && tree[k].r <= rch) {
55 tree[k].Min = min(tree[k].Min, num);
56 tree[k].f = 1;
57 return;
58 }
59 if (tree[k].f) down(k);
60 int mid = (tree[k].l + tree[k].r) / 2;
61 if (lch <= mid) update(k * 2, lch, rch, num);
62 if (rch > mid) update(k * 2 + 1, lch, rch, num);
63 }
64
65 int main() {
66 scanf("%d %d %d", &n, &m, &e);
67 e -= (m - 1);
68 for (int i = 0; i < n; i++) {
69 scanf("%d %d %d", &a[i].l, &a[i].r, &a[i].s);
70 a[i].l -= (m - 1);
71 a[i].r -= (m - 1);
72 }
73 sort(a, a + n);
74 build(1, 1, e);
75 for (int i = 0; i < n; i++) {
76 if (a[i].l == 1) {
77 update(1, a[i].l, a[i].r, a[i].s);
78 } else {
79 update(1, a[i].l, a[i].r, ask(1, a[i].l - 1) + a[i].s);
80 }
81 }
82 ll res = ask(1, e);
83 if (res == INF)
84 printf("-1
");
85 else
86 printf("%lld
", res);
87 }
END
以上是关于《挑战程序设计竞赛》课后练习题解集——3.4 熟练掌握动态规划的主要内容,如果未能解决你的问题,请参考以下文章
《挑战程序设计竞赛》课后练习题解集——2.3 记录结果再利用的“动态规划”
[转] AOJ 0525 Osenbei《挑战程序设计竞赛(第2版)》练习题答案