dp----状态压缩dp
《基于连通性的棋盘式状态压缩dp》
因为最典型的例子是放棋盘的一类问题所以我们叫他为棋盘式:典型例子如下:https://www.acwing.com/problem/content/1066/
像这类题目如果以dfs的方式思考是不好想的,不如从上到下枚举一下到底每一行的状态到底是如何的,
因为由题目可知:第i行的棋子摆放方式与第i-2行无关,仅仅与第i-1行和第i行相邻的有关。
所以我们可以将每一行放棋子的地方看做为1,不放的地方为0,于是就有了一行的状态可以以二进制的状态表示
因为最大的状态为2^n(n为行数),本题中也就为1024(10进制下)
所以我们完全可以枚举每一行的状态,然后再由上面的状态转移过来:
我们这里的棋子模型是8面的,所以在不同的两行(假设这一行为a,上一行为b)
1.同一行上放棋子不能相邻(用代码表示:(state & (state>>1))==0 , 相当于二进制下右移了,
因为规定1旁必定是0,出现(state & (state>>1))!=0,说明不符合规定了)
2.a,b两行放棋子不能在同一列上(用代码表示: (stateA & stateB)==0,符合不在同一列上),
不能在相邻列上(用代码表示: state=(stateA | stateB),然后判断state是否有相邻的1(用上面的1方法即可))
通过上面的两个条件的筛选我们就可以除去大部分状态,然后dp即可:
1 #include <iostream>
2 #include <algorithm>
3 #include <cstring>
4 #include <vector>
5 using namespace std;
6 const int N = 11;
7 // dp[i][j][k]的含义是在1~i层,第i层状态为j时,总共用了k个国王而且这些国王不相互攻击的方案数;
8 int n, g;
9 long long dp[N][2 << N][N * N];
10
11 // 在我以前的小国王设计中我感受到了明显的设计问题:
12 // 1.在for循环中去判断两个状态是否合法,而不是把他封装成函数
13 // 2.其实两个状态是否合法的判断我也写的不是很好:在判断两个状态是否可以在一起,只要求|,然后判断其是否有相邻的1即可
14 // 3.不仅仅是一行上合法状态我们可以预处理出来,而且两个状态是否可以在一起我们可以预处理出来
15
16 // rstate[]是保存状态相邻无1的合法状态
17 int rstate[2 << N], icnt = 0, oneNum[2 << N];
18 vector<int> mapstate[2 << N];
19 // check()是判断枚举的状态x是否有相邻的1
20 bool check(int x)
21 {
22 return !(x & (x >> 1));
23 }
24 // 判断上下两行状态是否满足相应条件
25 int legal(int a, int b)
26 {
27 if ((a & b) == 0 && check(a | b))
28 return true;
29 else
30 return false;
31 }
32 int count(int x)
33 {
34 int res = 0;
35 for (int i = 0; i < n; i++)
36 if ((x >> i) & 1 == 1)
37 res++;
38 return res;
39 }
40 int main()
41 {
42 cin >> n >> g;
43 for (int i = 0; i < (1 << n); i++)
44 if (check(i))
45 {
46 rstate[icnt++] = i;
47 oneNum[i] = count(i);
48 }
49 for (int i = 0; i < icnt; i++)
50 for (int j = 0; j < icnt; j++)
51 if (legal(rstate[i], rstate[j]))
52 mapstate[i].push_back(rstate[j]);
53
54 //注意这里:
55 dp[0][0][0] = 1;
56 for (int i = 1; i <= n; i++)
57 for (int j = 0; j < icnt; j++)
58 {
59 int nstate = rstate[j];
60 for (int u = 0; u < mapstate[j].size(); u++)
61 {
62 int pstate = mapstate[j][u];
63 for (int k = 0; k <= g; k++)
64 if (k >= oneNum[nstate])
65 dp[i][nstate][k] += dp[i - 1][pstate][k - oneNum[nstate]];
66 }
67 }
68
69 long long res = 0;
70 for (int i = 0; i < icnt; i++)
71 {
72 res += dp[n][rstate[i]][g];
73 }
74 cout << res;
75 return 0;
76 }
再如这个题:https://www.acwing.com/problem/content/329/
与上面不同的是这个棋子是4面的,而且除了有些土地不能用之外,其他条件与上面都相同。
分析方式也一样:
1 #include <iostream>
2 #include <algorithm>
3 #include <cstring>
4 #include <vector>
5 using namespace std;
6 const int N = 14, MOD = 1e8;
7 // dp[i][j]:表示在前1~i-1行中种完了玉米,然后第i行的状态为j的方案数
8 int dp[N][1 << N], g[N], m, n;
9 // 这里是保存一行中合法的状态
10 vector<int> rstate;
11 vector<int> mapstate[1 << N];
12 int main()
13 {
14 cin >> m >> n;
15 for (int i = 1; i <= m; i++)
16 for (int j = 1; j <= n; j++)
17 {
18 int t;
19 scanf("%d", &t);
20 g[i] += (!t) << (j - 1);
21 }
22
23 for (int i = 0; i < 1 << n; i++)
24 if (!(i & (i >> 1)))
25 rstate.push_back(i);
26
27 // 表示在i状态下可以在其上面的j状态
28 for (int i = 0; i < rstate.size(); i++)
29 for (int j = 0; j < rstate.size(); j++)
30 if (!(rstate[i] & rstate[j]))
31 mapstate[i].push_back(j);
32
33 dp[0][0] = 1;
34 for (int i = 1; i <= m; i++)
35 for (int a = 0; a < rstate.size(); a++)
36 {
37 int nowstate = rstate[a];
38 if (!(nowstate & g[i]))
39 for (int b = 0; b < mapstate[a].size(); b++)
40 {
41 int prestate = rstate[mapstate[a][b]];
42 dp[i][nowstate] = (dp[i][nowstate] + dp[i - 1][prestate]) % MOD;
43 }
44 }
45
46 int ans = 0;
47 for (int i = 0; i < rstate.size(); i++)
48 ans = (ans + dp[m][rstate[i]]) % MOD;
49 cout << ans;
50 return 0;
51 }
《集合式状态压缩dp》
看一下n很小,可以通过两个点确定一条抛物线:注意这条这条抛物线的a<0
我们用全部确定的抛物线看一下那些点在这个上面
然后这个问题变成了:我们要选那些抛物线才可以将猪猪全部覆盖(打下来),要求抛物线选的尽可能少
这个问题其实有个学名:“重复点覆盖问题”,这里我们就不去考虑这个问题
从dfs角度上可以怎么作?
我们可以将每一个猪猪看出01,如果其中一个猪猪被打下来了,那么我们将其计为1,否则为0
于是对于n个猪猪,可以用二进制代表,全部猪猪的状态
这个就是用状态压缩表示集合
写dfs的话为:dfs(state,cnt); 表示我用了cnt个抛物线(鸟),到达了state的状态
当state==(1<<n)-1的时候就 ans=min(ans,cnt)
状态的转移为dfs(state| 这条抛物线能够打到的猪猪,cnt+1);
1 #include <iostream> 2 #include <algorithm> 3 #include <cstring> 4 #include <vector> 5 #include <cmath> 6 using namespace std; 7 typedef pair<double, double> PDD; 8 #define x first 9 #define y second 10 const int N = 20; 11 // 步骤: 12 // 1.首先需要根据两点求出全部可能的抛物线,然后确定每一条抛物线上的能够覆盖的点 13 // 2.然后用dfs解决重复覆盖问题; 14 int ans, n, m, t; 15 // path[i][j]用来表示过点i和点j的抛物线能过的点集 16 int path[N][N], dp[1 << N]; 17 PDD pigs[N]; 18 // dfs(state,cnt):表示当到达状态state时用了cnt个抛物线(鸟) 19 /* void dfs(int state, int cnt) 20 { 21 if (state == ((1 << n) - 1)) 22 ans = min(ans, cnt); 23 int node; 24 // 先任选还没有被覆盖的点 25 for (int i = 1; i <= n; i++) 26 if ((state >> (i - 1))&1 == 0) 27 { 28 node = i; 29 break; 30 } 31 // 然后再枚举能够通往这个点的全部抛物线 32 for (int i = 1; i <= n; i++) 33 dfs(node | path[node][i], cnt + 1); 34 } */
但是dfs会超时,我们把dfs转换为for循环,用dp数组记录已经算到到达state状态的最小抛物线数
1 #include <iostream>
2 #include <algorithm>
3 #include <cstring>
4 #include <vector>
5 #include <cmath>
6 using namespace std;
7 typedef pair<double, double> PDD;
8 #define x first
9 #define y second
10 const int N = 20;
11 // 步骤:
12 // 1.首先需要根据两点求出全部可能的抛物线,然后确定每一条抛物线上的能够覆盖的点
13 // 2.然后用dfs解决重复覆盖问题;
14 int ans, n, m, t;
15 // path[i][j]用来表示过点i和点j的抛物线能过的点集
16 int path[N][N], dp[1 << N];
17 PDD pigs[N];
18 // dfs(state,cnt):表示当到达状态state时用了cnt个抛物线(鸟)
19 /* void dfs(int state, int cnt)
20 {
21 if (state == ((1 << n) - 1))
22 ans = min(ans, cnt);
23 int node;
24 // 先任选还没有被覆盖的点
25 for (int i = 1; i <= n; i++)
26 if ((state >> (i - 1))&1 == 0)
27 {
28 node = i;
29 break;
30 }
31 // 然后再枚举能够通往这个点的全部抛物线
32 for(int i=1;i<=n;i++)
33 dfs(state|path[node][i],cnt+1);
34 } */
35 int main()
36 {
37 cin >> t;
38 while (t--)
39 {
40 memset(path, 0, sizeof(path));
41 cin >> n >> m;
42 for (int i = 1; i <= n; i++)
43 scanf("%lf %lf", &pigs[i].x, &pigs[i].y);
44 for (int i = 1; i <= n; i++)
45 {
46 // 表示只过点i的抛物线
47 // 这里是解决当n==1时的边界情况
48 path[i][i] = 1 << (i - 1);
49 for (int j = 1; j <= n; j++)
50 {
51 double x1 = pigs[i].x, y1 = pigs[i].y;
52 double x2 = pigs[j].x, y2 = pigs[j].y;
53 if (fabs(x1 - x2) < 1e-8)
54 continue;
55 double a = (y1 / x1 - y2 / x2) / (x1 - x2);
56 double b = y1 / x1 - a * x1;
57 // 万分注意这里:一定要a<0才行
58 if (fabs(a - 0) < 1e-8 || a > 0)
59 continue;
60 int state = 0;
61 for (int k = 1; k <= n; k++)
62 {
63 double x3 = pigs[k].x;
64 double y3 = a * x3 * x3 + b * x3;
65 // 说明点k在这条抛物线上:
66 if (fabs(y3 - pigs[k].y) < 1e-8)
67 state += 1 << (k - 1);
68 }
69 path[i][j] = state;
70 }
71 }
72 memset(dp, 0x3f, sizeof(dp));
73 dp[0] = 0;
74 // 因为我们这里是根据dfs改成的for循环,在dfs中当state==1<<n-1时就该返回了
75 // 这里我们也一样,所以状态i不用枚举到1<<n-1;
76 for (int i = 0; i < (1 << n) - 1; i++)
77 {
78 int node;
79 for (int j = 1; j <= n; j++)
80 {
81 if (!((i >> (j - 1)) & 1))
82 {
83 node = j;
84 break;
85 }
86 }
87 // 从初状态i到达新状态用了path[node][j]这条抛物线
88 for (int j = 1; j <= n; j++)
89 dp[i | path[node][j]] = min(dp[i | path[node][j]], dp[i] + 1);
90 }
91 if (m == 1)
92 ans = (n % 3 == 0) ? n / 3 + 1 : n / 3 + 2;
93 else
94 ans = n;
95 ans = min(ans, dp[(1 << n) - 1]);
96 cout << ans << endl;
97 }
98 return 0;
99 }