前置知识点:二进制状态压缩,动态规划。
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个整数表示点i到j的距离(记为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路径的长度。
数据范围
1≤n≤20
0≤a[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时,表示输入终止,且该用例无需处理。
输出格式
每个测试用例输出一个结果,每个结果占一行。
数据范围
1≤N,M≤11
输入样例:
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 }