状压dp 持续更新

主宰稳场 提交于 2020-03-08 22:17:44

前置知识点:二进制状态压缩,动态规划。

 

1. AcWing 91 最短Hamilton路径 (https://www.acwing.com/problem/content/93/)

 

给定一张 n 个点的带权无向图,点从 0~n-1 标号,求起点 0 到终点 n-1 的最短Hamilton路径。 Hamilton路径的定义是从 0 到 n-1 不重不漏地经过每个点恰好一次。

输入格式

第一行输入整数n。

接下来n行每行n个整数,其中第i行第j个整数表示点ij的距离(记为a[i,j])。

对于任意的x,y,z,数据保证 a[x,x]=0,a[x,y]=a[y,x] 并且 a[x,y]+a[y,z]>=a[x,z]。

输出格式

输出一个整数,表示最短Hamilton路径的长度。

数据范围

1n20
0a[i,j]107

输入样例:

5
0 2 4 5 1
2 0 6 5 3
4 6 0 8 3
5 5 8 0 5
1 3 3 5 0

输出样例:

18

 

暴力跑一遍dfs:  O(N*N*N!)   枚举n个点的全排列求最小值:O(N*N!).

分析:

在暴力的方法中,有一些重复计算存在,比如在计算路径1->2->3->4->5与路径1->2->4->3->5的长度时,我们重复计算了路径1->2的长度。

所以我们可以考虑dp。

我们的已知信息是各点之间的路径长度,所以dp数组需要靠这些路径的长度来增加。

那么我们加上一个新的路径长度需要的条件有:

1.路径的端点一个已经到达,一个未到达,所以我们考虑使用当前点的到达情况作为状态划分变量。

2.当前的点是路径的端点,所以我们考虑使用当前所在的点作为状态划分变量。

dp[i][j]:点的到达情况为j,目前所在点是i的最短路径。

状态转移方程:dp[i][j]=min(dp[i^(1<<j)][k]+G[k][j]); (变量k枚举前一个点)。

起始状态:dp[1][0]=0;

最终状态:dp[(1<<n)-1][n-1];

 

时间复杂度:O(N*N*2^N)

代码:

 1 #include <iostream>
 2 #include <cstdio>
 3 #include <algorithm>
 4 #include <cstring>
 5 typedef long long ll;
 6 using namespace std;
 7 
 8 const int N=20;
 9 
10 int dp[1<<N][N];
11 int G[N][N];
12 int n;
13 
14 int main() {
15     scanf("%d",&n);
16     for(int i=0;i<n;i++) {
17         for(int j=0;j<n;j++) {
18             scanf("%d",&G[i][j]);
19         }
20     }
21     for(int i=1;i<(1<<n);i++) {
22         for(int j=0;j<n;j++) {
23             dp[i][j]=2e9;
24         }
25     }
26     dp[1][0]=0;
27     for(int i=1;i<(1<<n);i++) {
28         for(int j=0;j<n;j++) {
29             if((i>>j)&1) {
30                 for(int k=0;k<n;k++) {
31                     if(((i^(1<<j))>>k)&1) {
32                         dp[i][j]=min(dp[i][j],dp[i^(1<<j)][k]+G[k][j]);
33                     }
34                 }
35             }
36         }
37     }
38     printf("%d\n",dp[(1<<n)-1][n-1]);
39     return 0;
40 }

 

2.  AcWing 291. 蒙德里安的梦想 (https://www.acwing.com/problem/content/293/)

求把N*M的棋盘分割成若干个1*2的的长方形,有多少种方案。

例如当N=2,M=4时,共有5种方案。当N=2,M=3时,共有3种方案。

如下图所示:

输入格式

输入包含多组测试用例。

每组测试用例占一行,包含两个整数N和M。

当输入用例N=0,M=0时,表示输入终止,且该用例无需处理。

输出格式

每个测试用例输出一个结果,每个结果占一行。

数据范围

1N,M11

输入样例:

1 2
1 3
1 4
2 2
2 3
2 4
2 11
4 11
0 0

输出样例:

1
0
1
2
3
5
144
51205

这题问的是如何分割成1*2的方块,相当于如何用1*2的方块填满。

如果是随便填的话,我们需要考虑整个区域的状态,不太可做。我们可以一行一行填,先填满上面的在填下面的,这样就简单得多,所以考虑以当前行数作为一个状态划分变量。我们发现,在我们填满一行后并不一定要仅填满当前一行,而是可以凸到下面一行的,不同的凸法会对下一行造成影响,所以考虑以填当前行时的凸法作为一个状态划分变量。

上一行凸法与当前行凸法之间的关系:

1.上一行凸的一列当前行不凸。

2.当前行凸的一列上一行不凸。

确定完当前行突出的部分以及上一行凸下来的部分之后,剩下的部分只能用来放横着的方块,放横着的方块需要满足剩余的部分没有连续奇数个的剩余。

我们设1为凸,0为不凸,设当前行的凸状态为j,上一行的凸状态为k,则剩余的部分为j|k中0的部分。我们提前计算好该状态用来放下横着的方块是否可行,避免重复计算。

代码:

 1 #include <iostream>
 2 #include <cstdio>
 3 #include <algorithm>
 4 #include <cstring>
 5 typedef long long ll;
 6 using namespace std;
 7 
 8 const int N=11;
 9 
10 ll dp[N+1][1<<N];
11 bool able[1<<N];
12 int n,m;
13 
14 int main() {
15     while(scanf("%d%d",&n,&m),(n||m)) {
16         if((n*m)&1) {
17             printf("0\n"); continue;//n*m为奇数判断方案数为0.
18         }
19         memset(dp,0,sizeof(dp));
20         dp[0][0]=1;
21         for(int i=0;i<(1<<m);i++) {//提前计算是否可行。
22             int cnt=0; able[i]=true;
23             for(int j=0;j<m;j++) {
24                 if((i>>j)&1) {
25                     if(cnt&1) able[i]=false;
26                     cnt=0;
27                 }else cnt++;
28             }
29             if(cnt&1) able[i]=false;
30         }
31         for(int i=1;i<=n;i++) {
32             for(int j=0;j<(1<<m);j++) {
33                 for(int k=0;k<(1<<m);k++) {
34                     if(!(k&j)&&able[j|k]) {//!(k&j)判断是否满足上面说的行之间的两个关系。
35                         dp[i][j]+=dp[i-1][k];
36                     }
37                 }
38             }
39         }
40         printf("%lld\n",dp[n][0]);
41     }
42     return 0;
43 }

 

3. AcWing 164. 可达性统计

给定一张N个点M条边的有向无环图,分别统计从每个点出发能够到达的点的数量。

输入格式

第一行两个整数N,M,接下来M行每行两个整数x,y,表示从x到y的一条有向边。

输出格式

输出共N行,表示每个点能够到达的点的数量。

数据范围

1N,M30000

输入样例:

10 10
3 8
2 3
2 5
5 9
5 9
2 3
3 9
4 8
2 10
4 9

输出样例:

1
6
3
3
2
1
1
1
1
1

这题其实是一道拓扑排序的题目,但我也不知道为什么是拓扑排序的题目,因为这题用dfs来算和拓扑排序之后再算似乎没有本质的区别。

分析:

一看到数据量,不能用暴力直接从每个点开始dfs计算到达了多少个点。

看到有向无环图,想到了先生成个dfs搜索生成树,然后在树上加些边看看能不能发现什么东西,最终失败。

于是开始考虑dp,我们发现一个点能到达的点等于它本身+他下一步能到的点所能到的点。但是显然无法直接累加,因为会有重复,于是考虑到二进制状态压缩。

得到状态转移方程:able[u]=(able[v1]|able[v2]|...|able[vn])|(1<<(u-1));

这题的点有30000个,所以用数字记录状态需要30000位,显然做不到,开bool数组算起来太慢,所以用bitset。(bitset用法 https://www.cnblogs.com/magisk/p/8809922.html

代码:

 1 #include <iostream>
 2 #include <cstdio>
 3 #include <algorithm>
 4 #include <cstring>
 5 #include <bitset>
 6 typedef long long ll;
 7 using namespace std;
 8 
 9 const int N=3e4+5;
10 
11 bitset <N> able[N];
12 
13 int E[N<<1],fir[N],nex[N<<1],tot=0;
14 bool deg[N];
15 bool vis[N];
16 int n,m;
17 
18 void connect(int from,int to) {
19     E[tot]=to;
20     nex[tot]=fir[from];
21     fir[from]=tot++;
22 }
23 
24 void dfs(int now) {
25     vis[now]=true;
26     for(int i=fir[now];i!=-1;i=nex[i]) {
27         int to=E[i];
28         if(!vis[to]) dfs(to);
29         able[now]|=able[to];
30     }
31 }
32 
33 int main() {
34     memset(fir,-1,sizeof(fir));
35     scanf("%d%d",&n,&m);
36     for(int i=1;i<=m;i++) {
37         int x,y;
38         scanf("%d%d",&x,&y);
39         connect(x,y);
40         deg[y]=true;
41     }
42     for(int i=1;i<=n;i++) {
43         able[i][i-1]=true;
44     }
45     for(int i=1;i<=n;i++) {
46         if(!deg[i]) dfs(i);
47     }
48     for(int i=1;i<=n;i++) {
49         printf("%d\n",able[i].count());
50     }
51     return 0;
52 }

 

4. POJ 1185(https://vjudge.net/problem/POJ-1185)

司令部的将军们打算在N*M的网格地图上部署他们的炮兵部队。一个N*M的地图由N行M列组成,地图的每一格可能是山地(用"H" 表示),也可能是平原(用"P"表示),如下图。在每一格平原地形上最多可以布置一支炮兵部队(山地上不能够部署炮兵部队);一支炮兵部队在地图上的攻击范围如图中黑色区域所示: 


如果在地图中的灰色所标识的平原上部署一支炮兵部队,则图中的黑色的网格表示它能够攻击到的区域:沿横向左右各两格,沿纵向上下各两格。图上其它白色网格均攻击不到。从图上可见炮兵的攻击范围不受地形的影响。 
现在,将军们规划如何部署炮兵部队,在防止误伤的前提下(保证任何两支炮兵部队之间不能互相攻击,即任何一支炮兵部队都不在其他支炮兵部队的攻击范围内),在整个地图区域内最多能够摆放多少我军的炮兵部队。 

Input

第一行包含两个由空格分割开的正整数,分别表示N和M; 
接下来的N行,每一行含有连续的M个字符('P'或者'H'),中间没有空格。按顺序表示地图中每一行的数据。N <= 100;M <= 10。

Output

仅一行,包含一个整数K,表示最多能摆放的炮兵部队的数量。

Sample Input

5 4
PHPP
PPHH
PPPP
PHPP
PHHP

Sample Output

6

 

分析:

这题有点类似AcWing 291. 蒙德里安的梦想,只不过这题要考虑2行的状态,即当前行的状态与往上2行的状态有关。

dp[i][j][k]:当前行数为i,当前行与上一行的炮兵放置状态为j,k的最大炮兵放置数量。

我们事先计算出仅考虑当前行的可放置方案(不考虑地形以及往上2行的放置状态,只考虑同一行之间会不会互相攻击)。

然后再计算出仅考虑2行的可放置方案(只考虑同一行之间以及相邻两行之间会不会互相攻击)。

接下来的步骤见于代码:

 1 #include <iostream>
 2 #include <cstdio>
 3 #include <algorithm>
 4 #include <cstring>
 5 #include <bitset>
 6 #include <vector>
 7 typedef long long ll;
 8 using namespace std;
 9 
10 struct Status {//仅考虑2行的可放置方案。
11     int up;//上面的一行。
12     int down;//下面的一行。
13 };
14 
15 int get[65];//事先计算好每种1行的状态有多少个炮兵。
16 ll dp[105][65][65];
17 
18 int main() {
19     int n,m;
20     scanf("%d%d",&n,&m);
21     bitset <10> dixing[n];//储存地形。
22     for(int i=1;i<=n;i++) dixing[i]=0;
23     for(int i=1;i<=n;i++) {
24         char s[15];
25         scanf("%s",s);
26         for(int j=0;j<m;j++) {
27             if(s[j]=='P') dixing[i][j]=true;
28         }
29     }
30     vector <int> able;//仅考虑当前行的可放置方案
31     for(int i=0;i<(1<<m);i++) {
32         bool flag=true;
33         for(int j=0;j<m;j++) {
34             if((i>>j)&1) {//找到一个炮兵,下面四个判断用来判断左,右2格是否有其他炮兵。
35                 if(j>=1) {
36                     if((i>>(j-1))&1) {
37                         flag=false;break;
38                     }
39                 }
40                 if(j>=2) {
41                     if((i>>(j-2))&1) {
42                         flag=false;break;
43                     }
44                 }
45                 if(j<=m-2) {
46                     if((i>>(j+1))&1) {
47                         flag=false;break;
48                     }
49                 }
50                 if(j<=m-3) {
51                     if((i>>(j+2))&1) {
52                         flag=false;break;
53                     }
54                 }
55             }
56         }
57         if(flag) able.push_back(i);
58     }
59     for(int i=0;i<able.size();i++) {
60         bitset <10> tmp=able[i];
61         get[i]=tmp.count();
62     }
63     vector <Status> status;
64     for(int i=0;i<able.size();i++) {
65         for(int j=0;j<able.size();j++) {
66             if((able[i]&able[j])!=0) continue;
67             status.push_back((Status){i,j});
68         }
69     }
70     for(int i=1;i<=n;i++) {
71         for(int j=0;j<able.size();j++) {
72             bitset <10> tmp=able[j];
73             bool flag=true;
74             for(int k=0;k<m;k++) {
75                 if(dixing[i][k]==0&&tmp[k]==1) {//判断当前行地形是否支持该放置方案。
76                     flag=false;
77                     break;
78                 }
79             }
80             if(!flag) continue;
81             for(int k=0;k<status.size();k++) {
82                 if((able[j]&able[status[k].up])||((able[j]&able[status[k].down]))) continue;//判断当前行的放置是否与上2行冲突。
83                 dp[i][j][status[k].down]=max(dp[i][j][status[k].down],dp[i-1][status[k].down][status[k].up]+get[j]);
84             }
85         }
86     }
87     ll ans=0;
88     for(int i=0;i<status.size();i++) {
89         ans=max(ans,dp[n][status[i].down][status[i].up]);
90     }
91     printf("%lld\n",ans);
92     return 0;
93 }

 


标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!