动态规划之状态压缩专题
2017-11-27 21:26
429 查看
态压缩动态规划(简称状压dp)是另一类非常典型的动态规划,通常使用在NP问题的小规模求解中,虽然是指数级别的复杂度,但速度比搜索快,其思想非常值得借鉴。
为了更好的理解状压dp,首先介绍位运算相关的知识。
1.’&’符号,x&y,会将两个十进制数在二进制下进行与运算,然后返回其十进制下的值。例如3(11)&2(10)=2(10)。
2.’|’符号,x|y,会将两个十进制数在二进制下进行或运算,然后返回其十进制下的值。例如3(11)|2(10)=3(11)。
3.’^’符号,x^y,会将两个十进制数在二进制下进行异或运算,然后返回其十进制下的值。例如3(11)^2(10)=1(01)。
4.’<<’符号,左移操作,x<<2,将x在二进制下的每一位向左移动两位,最右边用0填充,x<<2相当于让x乘以4。相应的,’>>’是右移操作,x>>1相当于给x/2,去掉x二进制下的最有一位。
这四种运算在状压dp中有着广泛的应用,常见的应用如下:
1.判断一个数字x二进制下第i位是不是等于1。
方法:if ( ( ( 1 << ( i - 1 ) ) & x ) > 0)
将1左移i-1位,相当于制造了一个只有第i位上是1,其他位上都是0的二进制数。然后与x做与运算,如果结果>0,说明x第i位上是1,反之则是0。
2.将一个数字x二进制下第i位更改成1。
方法:x = x | ( 1<<(i-1) )
证明方法与1类似,此处不再重复证明。
3.把一个数字二进制下最靠右的第一个1去掉。
方法:x=x&(x-1)
感兴趣的读者可以自行证明。
位运算在状压dp中用途十分广泛。
1.(k&-k)在状态压缩和树状数组中都经常能够看到,那么(k&-k)的值是什么含义呢?
这个值是把k的二进制的高位1全部清空,只留下最低位的1,当然如果只有一位1,则保留等于k本身。该操作就是留下k二进制数中最低位的一个1
2.两种相等的形式:i-(i&-i) = i^(i&-i)
他们都表示减去最低位的1,再返回值
3.状态压缩的方法,就是利用二进制数的零一进行模拟。零代表没取,一代表取了。时间复杂度O(2^n)只能用在n小于20的情况下。首先在存储每个元素的时候就按照(1,10,100,1000)的方法来存储,需要枚举所有的子集时可以利用sum[i] = sum[i-(i&-i)] + a[i & -i] 每个sum[i],都表示i在二进制中1取零不取。可以进行初始化,算出每个i的二进制一的个数。
5.一定要注意的问题:
原数组,存储所有子集的数组,还有用来存储任意个数二进制个数的数组大小都要开到2^n次方。另,当n <= 20时,时间空间都可以容纳2^20
【解析】根据题意,把每一行的状态用二进制的数表示,0代表不在这块放牛,1表示在这一块放牛。首先很容易看到,每一行的状态要符合牧场的硬件条件,即牛必须放在能放牧的方格上。这样就能排除一些状态。另外,牛与牛之间不能相邻,这样就要求每一行中不能存在两个相邻的1,这样也能排除很多状态。然后就是根据上一行的状态转移到当前行的状态的问题了。必须符合不能有两个1在同一列(两只牛也不能竖着相邻)的条件。这样也能去掉一些状态。然后,上一行的所有符合条件的状态的总的方案数就是当前行该状态的方案数。
【状态表示】dp[state][i]:在状态为state时,到第i行符合条件的可以放牛的方案数
【状态转移方程】dp[state][i] =Sigma dp[state’][i-1] (state’为符合条件的所有状态)
【DP边界条件】首行放牛的方案数dp[state][1] =1(state符合条件) OR 0 (state不符合条件)
【解析】可以发现,对于每一行放大炮的状态,只与它上面一行和上上一行的状态有关,每一行用状态压缩的表示方法,0表示不放大炮,1表示放大炮,同样的,先要满足硬件条件,即有的地方不能放大炮,然后就是每一行中不能有两个1的距离小于2(保证横着不互相攻击),这些要预先处理一下。然后就是状态表示和转移的问题了,因为是和前两行的状态有关,所以要开个三维的数组来表示状态,当前行的状态可由前两行的状态转移而来。即如果当前行的状态符合前两行的约束条件(不和前两行的大炮互相攻击),则当前行的最大值就是上一个状态的值加上当前状态中1的个数(当前行放大炮的个数)
【状态表示】dp[i][j][k] 表示第i行状态为k,第i-1状态为j时的最大炮兵个数。
【状态转移方程】dp[i][k][t] =max(dp[i][k][t],dp[i-1][j][k]+num[t]); num[t]为t状态中1的个数
【DP边界条件】dp[1][1][i] =num[i] 状态i能够满足第一行的硬件条件(注意:这里的i指的是第i个状态,不是一个二进制数,开一个数组保存二进制状态)
【解析】可以用全排列做,求出一个最短的距离即可。或者用状态压缩DP.用一个二进制数表示城市是否走过
【状态表示】dp[state][i]表示到达i点状态为state的最短距离
【状态转移方程】dp[state][i] =min{dp[state][i],dp[state’][j]+dis[j][i]} dis[j][i]为j到i的最短距离
【DP边界条件】dp[state][i] =dis[0][i] state是只经过i的状态
【代码】
【解析】和tsp问题相同,类似于上面那个题
【状态表示】【状态转移方程】同上题,具体见代码
【代码】
【解析】发现每个点的状态由前面两个点确定,用DP(S,A,B)表示状态为S时,当前到达A,而上一个点是B时的最大得分,这个状态由DP(S’,B,C)通过从B走到A得到,S’=S-(1<<A),即S’状态就是经过B和C但不经过A的一个状态,C是不同于A和B的一个点。
【状态转移】dp[S][A][B] =max(dp[S][A][B],dp[S’][B][C]+temp) 这里的temp指的是加上的得分即Vb*Va+Va,如果构成三角关系(即A和C间有边),temp就要再加上Vb*Va*Vc.
【边界条件】DP((1<<A)+(1<<B),A,B)=Va+Vb+Va*Vb(A和B间有边)表示
【代码】
【题目解析】用10位二进制表示气体是否存在,0表示存在,1表示不存在,S(上一个状态)中的两种气体碰撞并且有一种消失,可以得到newS的状态(状态转移)
【状态表示】dp[state] 状态为state时的最大能量
【转移方程】dp[state] = max(dp[state],dp[state’]+a[i][j])
【边界条件】dp[i] = 0;
【代码】
【解析】如果是横着的就定义11,如果竖着的定义为竖着的01,这样按行dp只需要考虑两件事儿,当前行&上一行,是不是全为1,不是说明竖着有空(不可能出现竖着的00),另一个要检查当前行里有没有横放的,但为奇数的1。
【状态表示】dp[state][i]第i行状态为state时候的方案数
【转移方程】dp[state][i] += dp[state’][i-1] state’为i-1行的,不与i行状态state冲突的状态
【边界条件】第一行 符合条件的状态记为1 即dp[state][0] = 1;
【代码】
【题目解析】机器人从出发点出发要求走过所有的Y,因为点很少,所以就能想到经典的TSP问题(起初我也想到了),但关于G点能充电的问题不知道怎么办?G点可以充电,到达G点就把当前能量更新为电池容量然后继续走。因为每个G点只能充一次电,这就好像TSP中的每个点只能走一次一样(G和Y都可以走多次,但走到G充电后,该点就变为了S,而走到Y关上开关以后,Y也变成了S。这是一个很巧妙地想法,所以要求Y点只能关一次开关,G点只能充一次电,这就是TSP了。),然后就是二分答案了,用状压DP判定当前电池容量的情况下是否能符合条件。
【状态表示】dp[s][i]表示到达当前i点状态为s时最大的剩余的能量
【转移方程】同TSP问题了
【边界条件】dp[1<<sid][sid] = rongliang.即出发点的能量就是电池容量.
【代码】
为了更好的理解状压dp,首先介绍位运算相关的知识。
1.’&’符号,x&y,会将两个十进制数在二进制下进行与运算,然后返回其十进制下的值。例如3(11)&2(10)=2(10)。
2.’|’符号,x|y,会将两个十进制数在二进制下进行或运算,然后返回其十进制下的值。例如3(11)|2(10)=3(11)。
3.’^’符号,x^y,会将两个十进制数在二进制下进行异或运算,然后返回其十进制下的值。例如3(11)^2(10)=1(01)。
4.’<<’符号,左移操作,x<<2,将x在二进制下的每一位向左移动两位,最右边用0填充,x<<2相当于让x乘以4。相应的,’>>’是右移操作,x>>1相当于给x/2,去掉x二进制下的最有一位。
这四种运算在状压dp中有着广泛的应用,常见的应用如下:
1.判断一个数字x二进制下第i位是不是等于1。
方法:if ( ( ( 1 << ( i - 1 ) ) & x ) > 0)
将1左移i-1位,相当于制造了一个只有第i位上是1,其他位上都是0的二进制数。然后与x做与运算,如果结果>0,说明x第i位上是1,反之则是0。
2.将一个数字x二进制下第i位更改成1。
方法:x = x | ( 1<<(i-1) )
证明方法与1类似,此处不再重复证明。
3.把一个数字二进制下最靠右的第一个1去掉。
方法:x=x&(x-1)
感兴趣的读者可以自行证明。
位运算在状压dp中用途十分广泛。
1.(k&-k)在状态压缩和树状数组中都经常能够看到,那么(k&-k)的值是什么含义呢?
这个值是把k的二进制的高位1全部清空,只留下最低位的1,当然如果只有一位1,则保留等于k本身。该操作就是留下k二进制数中最低位的一个1
2.两种相等的形式:i-(i&-i) = i^(i&-i)
他们都表示减去最低位的1,再返回值
3.状态压缩的方法,就是利用二进制数的零一进行模拟。零代表没取,一代表取了。时间复杂度O(2^n)只能用在n小于20的情况下。首先在存储每个元素的时候就按照(1,10,100,1000)的方法来存储,需要枚举所有的子集时可以利用sum[i] = sum[i-(i&-i)] + a[i & -i] 每个sum[i],都表示i在二进制中1取零不取。可以进行初始化,算出每个i的二进制一的个数。
//bc[i]表示i的二进制表示中一的个数是多少 bc[0] = 0; for (i=1; i<(1<<20); i++) bc[i] = bc[i-(i&-i)] + 1;
for (i=0; i<m; i++) scanf("%d", &tmp[1<<i]);//tmp存储的时候就是按照1左移存储的
sum[0] = 0; for (i=1; i<(1<<m); i++) sum[i] = sum[i-(i&-i)] + tmp[i&-i];/*每次都保证减去最低位后的状态是已经求出的,或者状态是空sum[0]。比如从1000开始到1001(1000求出)到1010(1000求出)到1011(减去最低位后为1010也已求出)。那么为什么地位减去后一定是已求出的呢,因为减去小于其本身,sum[i]又是顺序遍历的 此处还有问题,为什么是不重不漏的呢?每次到临界值(1,10,100,1000)时sum[i]的值都等于tmp[i].当减去最低位后,不肯能是等于它本身的,此时又再加上了另一个数, 注意此处的1,0是二进制。实际上是sum[1] = tmp[1],sum[10] = tmp[10],sum[11] = sum[10] + temp[1] = temp[10] + temp[1],以此类推sum[1111] = temp[1] + temp[10] + temp[100] + temp[1000] 其实很简单的啦,每次sum[i]表示的都是对于i的二进制数来说0,代表取到1代表取不到。肯定是全部的状态(不包括都取不到的,那种是sum[0]的情况)*/
5.一定要注意的问题:
原数组,存储所有子集的数组,还有用来存储任意个数二进制个数的数组大小都要开到2^n次方。另,当n <= 20时,时间空间都可以容纳2^20
【POJ3254】Corn Fields
【在线测试提交传送门】
【问题描述】
农夫有一块地,被划分为m行n列 N (1 ≤ M ≤ 12; 1 ≤ N ≤ 12) 大小相等的格子,其中一些格子是可以放牧的(用1标记),农夫可以在这些格子里放牛,其他格子则不能放牛(用0标记),并且要求不可以使相邻格子都有牛。现在输入数据给出这块地的大小及可否放牧的情况,求该农夫有多少种放牧方案可以选择(注意:任何格子都不放也是一种选择,不要忘记考虑!)
【输入格式】
第一行,两个整数,表示M和N。 第2到第M+1行,每行N个整数,1表示可以放牧,0表示不可以放牧。
【输出格式】
一行,一个整数,表示能放牧方案数,将结果对100,000,000取模。
【输入样例1】
2 3 1 1 1 0 1 0
【输出样例1】
9
【解析】根据题意,把每一行的状态用二进制的数表示,0代表不在这块放牛,1表示在这一块放牛。首先很容易看到,每一行的状态要符合牧场的硬件条件,即牛必须放在能放牧的方格上。这样就能排除一些状态。另外,牛与牛之间不能相邻,这样就要求每一行中不能存在两个相邻的1,这样也能排除很多状态。然后就是根据上一行的状态转移到当前行的状态的问题了。必须符合不能有两个1在同一列(两只牛也不能竖着相邻)的条件。这样也能去掉一些状态。然后,上一行的所有符合条件的状态的总的方案数就是当前行该状态的方案数。
【状态表示】dp[state][i]:在状态为state时,到第i行符合条件的可以放牛的方案数
【状态转移方程】dp[state][i] =Sigma dp[state’][i-1] (state’为符合条件的所有状态)
【DP边界条件】首行放牛的方案数dp[state][1] =1(state符合条件) OR 0 (state不符合条件)
#include <cstdio> #include <cstring> using namespace std; #define mod 100000000 int M,N,top = 0; int state[600],num[110]; int dp[20][600]; int cur[20]; inline bool ok(int x){ if(x&x<<1)return 0; return 1; } void init(){ top = 0; int total = 1 << N; for(int i = 0; i < total; ++i){ if(ok(i))state[++top] = i; } } inline bool fit(int x,int k){ if(x&cur[k])return 0; return 1; } inline int jcount(int x) { int cnt=0; while(x) { cnt++; x&=(x-1); } return cnt; } int main(){ while(scanf("%d%d",&M,&N)!= EOF){ init(); memset(dp,0,sizeof(dp)); for(int i = 1; i <= M; ++i){ cur[i] = 0; int num; for(int j = 1; j <= N; ++j){ scanf("%d",&num); if(num == 0)cur[i] +=(1<<(N-j)); } } for(int i = 1;i <= top;i++){ if(fit(state[i],1)){ dp[1][i] = 1; } } for(int i = 2; i <= M; ++i){ for(int k = 1; k <= top; ++k){ if(!fit(state[k],i))continue; for(int j = 1; j <= top ;++j){ if(!fit(state[j],i-1))continue; if(state[k]&state[j])continue; dp[i][k] = (dp[i][k] +dp[i-1][j])%mod; } } } int ans = 0; for(int i = 1; i <= top; ++i){ ans = (ans + dp[M][i])%mod; } printf("%d\n",ans); } }
【POJ1185】炮兵阵地–经典状压DP
【在线测试提交传送门】
【问题描述】
司令部的将军们打算在N*M的网格地图上部署他们的炮兵部队。一个N*M的地图由N行M列组成,地图的每一格可能是山地(用"H" 表示),也可能是平原(用"P"表示),如下图。在每一格平原地形上最多可以布置一支炮兵部队(山地上不能够部署炮兵部队);一支炮兵部队在地图上的攻击范围如图中黑色区域所示。
如果在地图中的灰色所标识的平原上部署一支炮兵部队,则图中的黑色的网格表示它能够攻击到的区域:沿横向左右各两格,沿纵向上下各两格。图上其它白色网格均攻击不到。从图上可见炮兵的攻击范围不受地形的影响。 现在,将军们规划如何部署炮兵部队,在防止误伤的前提下(保证任何两支炮兵部队之间不能互相攻击,即任何一支炮兵部队都不在其他支炮兵部队的攻击范围内),在整个地图区域内最多能够摆放多少我军的炮兵部队。
【输入格式】
第一行包含两个由空格分割开的正整数,分别表示N和M; 接下来的N行,每一行含有连续的M个字符('P'或者'H'),中间没有空格。按顺序表示地图中每一行的数据。N ≤ 100;M ≤ 10。
【输出格式】
仅一行,包含一个整数K,表示最多能摆放的炮兵部队的数量。
【输入样例】
5 4 PHPP PPHH PPPP PHPP PHHP
【输出样例】
6
【解析】可以发现,对于每一行放大炮的状态,只与它上面一行和上上一行的状态有关,每一行用状态压缩的表示方法,0表示不放大炮,1表示放大炮,同样的,先要满足硬件条件,即有的地方不能放大炮,然后就是每一行中不能有两个1的距离小于2(保证横着不互相攻击),这些要预先处理一下。然后就是状态表示和转移的问题了,因为是和前两行的状态有关,所以要开个三维的数组来表示状态,当前行的状态可由前两行的状态转移而来。即如果当前行的状态符合前两行的约束条件(不和前两行的大炮互相攻击),则当前行的最大值就是上一个状态的值加上当前状态中1的个数(当前行放大炮的个数)
【状态表示】dp[i][j][k] 表示第i行状态为k,第i-1状态为j时的最大炮兵个数。
【状态转移方程】dp[i][k][t] =max(dp[i][k][t],dp[i-1][j][k]+num[t]); num[t]为t状态中1的个数
【DP边界条件】dp[1][1][i] =num[i] 状态i能够满足第一行的硬件条件(注意:这里的i指的是第i个状态,不是一个二进制数,开一个数组保存二进制状态)
#include <cstdio> #include <cstring> using namespace std; #define max(a,b) (a) > (b) ? (a) : (b) int N,M; char map[110][20],num[110],top; int stk[70],cur[110]; int dp[110][70][70]; inline bool ok(int x){ //判断该状态是否合法,即不存在相邻的1之间的距离小于3的 if(x&(x<<1)) return 0; if(x&(x<<2)) return 0; return 1; } //找到所有可能的合法状态,最多60种 inline void jinit() { top=0; int i,total=1<<N; for(i=0;i<total;i++) if(ok(i)) stk[++top]=i; } //判断状态x是否与第k行匹配 inline bool fit(int x,int k) { if(cur[k]&x) return 0; return 1; } //数一个整型数x的二进制中1的个数(用于初始化) inline int jcount(int x) { int cnt=0; while(x) { cnt++; x&=(x-1); } return cnt; } int main(){ while(scanf("%d%d",&M,&N) != EOF){ if(N == 0 && M == 0)break; jinit(); for(int i = 1; i <= M; ++i)scanf("%s",map[i]+1); for(int i = 1; i <= M; ++i) for(int j = 1; j <= N; ++j){ cur[i]=0; for(j=1;j<=N;j++){ if(map[i][j]=='H')cur[i]+=(1<<(j-1)); } } memset(dp,-1,sizeof(dp)); //初始化第一行状态 for(int i = 1;i <= top;i++){ num[i]=jcount(stk[i]); if(fit(stk[i],1)) dp[1][1][i]=num[i]; } int i,t,j,k; for(i = 2;i <= M;i++){ for(t = 1;t <= top;t++){ if(!fit(stk[t],i)) continue; for(j = 1;j <= top;j++) { if(stk[t]&stk[j])continue; for(k = 1;k <= top;k++) { if(stk[t]&stk[k])continue; if(dp[i-1][j][k]==-1)continue; dp[i][k][t] =max(dp[i][k][t],dp[i-1][j][k]+num[t]); } } } } int ans = 0; for(i = 1; i <= M; ++i) for(j = 1; j <= top; ++j) for(k = 1; k <= top; ++k) ans = max(ans,dp[i][j][k]); printf("%d\n",ans); } return 0; }
【POJ3311】Hie With The Pie
【在线测试提交传送门】
【问题描述】
披萨送餐员从披萨店出发,送n份披萨到n个不同的送餐点,并返回到披萨店。披萨店的编号为0,n个送餐点的编号为1到n。他可以经过同一个送餐点多次,但必须把每份披萨都送到。求经过的最短路程。
【输入格式】
第1行,一个整数n(1 ≤ n ≤ 10),表示有n个送餐点。 接下来n+1行,每行n+1个数,分别表示披萨店及各个送餐点自己的距离。
【输出格式】
一行,一个整数,表示经过的最短路程。
【输入样例1】
3 0 1 10 10 1 0 1 2 10 1 0 10 10 2 10 0
【输出样例1】
8
【解析】可以用全排列做,求出一个最短的距离即可。或者用状态压缩DP.用一个二进制数表示城市是否走过
【状态表示】dp[state][i]表示到达i点状态为state的最短距离
【状态转移方程】dp[state][i] =min{dp[state][i],dp[state’][j]+dis[j][i]} dis[j][i]为j到i的最短距离
【DP边界条件】dp[state][i] =dis[0][i] state是只经过i的状态
【代码】
#include<iostream> #define INF 100000000 using namespace std; int dis[12][12]; int dp[1<<11][12]; int n,ans,_min; int main() { while(scanf("%d",&n) && n) { for(int i = 0;i <= n;++i) for(int j = 0;j <= n;++j) scanf("%d",&dis[i][j]); for(int k = 0;k <= n;++k) for(int i = 0;i <= n;++i) for(int j = 0;j <=n;++j) if(dis[i][k] + dis[k][j]< dis[i][j]) dis[i][j] = dis[i][k] +dis[k][j]; for(int S = 0;S <= (1<<n)-1;++S)//枚举所有状态,用位运算表示 for(int i = 1;i <= n;++i) { if(S & (1<<(i-1)))//状态S中已经过城市i { if(S ==(1<<(i-1))) dp[S][i] =dis[0][i];//状态S只经过城市I,最优解自然是从0出发到i的dis,这也是DP的边界 else//如果S有经过多个城市 { dp[S][i] = INF; for(int j = 1;j <=n;++j) { if(S &(1<<(j-1)) && j != i)//枚举不是城市I的其他城市 dp[S][i] =min(dp[S^(1<<(i-1))][j] + dis[j][i],dp[S][i]); //在没经过城市I的状态中,寻找合适的中间点J使得距离更短 } } } } ans = dp[(1<<n)-1][1] + dis[1][0]; for(int i = 2;i <= n;++i) if(dp[(1<<n)-1][i] + dis[i][0] < ans) ans = dp[(1<<n)-1][i] +dis[i][0]; printf("%d\n",ans); } return 0; }
【HDU3001】Traveling
【在线测试提交传送门】
【问题描述】
有n个旅游景点,编号为1到n,初始时可以在任意一个景点,每个景点必须被参观,但不能超过2次。景点间一共有m条收费的道路。请最少收费。
【输入格式】
第一行,两个整数,n(1≤n≤10)和m,分别表示景点个数和道路数量。 接下来m行,每行3个整数a,b,c(≤a,b≤n),表示景点a和b之间有一条收费为c的道路。道路为双向边,且可能存在重边。
【输出格式】
输出共一行,一个整数,表示最小费用,若无解则输出-1。
【输入样例1】
2 1 1 2 100
【输出样例1】
100
【输入样例2】
3 3 1 2 3 1 3 4 2 3 10
【输出样例2】
7
【解析】和tsp问题相同,类似于上面那个题
【状态表示】【状态转移方程】同上题,具体见代码
【代码】
#include <cstdio> #include <cstring> #define INF 0x1f1f1f1f //刚发现这里写0x1f1f1f跑的比0x1f1f1f1f差不多慢了一倍!Orz~ #define min(a,b) (a) < (b) ? (a) : (b) using namespace std; int N,M; int tri[12] ={0,1,3,9,27,81,243,729,2187,6561,19683,59049}; int dig[59050][11]; //dig[state][k_dig] 状态state的第k位是多少 int edge[11][11],dp[59050][11]; int main(){ for(int i = 0; i < 59050; ++i){ int t = i; for(int j = 1; j <= 10; ++j){ dig[i][j] = t%3; t /= 3; if(t == 0)break; } } while(scanf("%d%d",&N,&M) != EOF){ memset(edge,INF,sizeof(edge)); int a,b,c; while(M --){ scanf("%d%d%d",&a,&b,&c); if(c < edge[a][b])edge[a][b] = edge[b][a] = c; } memset(dp,INF,sizeof(dp)); for(int i = 1; i <= N; ++i)dp[tri[i]][i] = 0; int ans = INF; for(int S = 0; S < tri[N+1]; ++S){ int visit_all = 1; for(int i = 1; i <= N; ++i){ if(dig[S][i] == 0)visit_all = 0; if(dp[S][i] == INF)continue; for(int j = 1; j <= N; ++j){ if(i == j)continue; if(edge[i][j] == INF ||dig[S][j] >= 2)continue; int newS = S + tri[j]; dp[newS][j] =min(dp[newS][j],dp[S][i] + edge[i][j]); } } if(visit_all){ for(int j = 1; j <= N; ++j) ans = min(ans,dp[S][j]); } } if(ans == INF){ puts("-1"); continue; } printf("%d\n",ans); } return 0; }
【POJ2288】Islands and Bridge
【在线测试提交传送门】
【问题描述】
有n座岛和m座桥,求一条汉密尔顿回路,即访问每座到恰好一次。每座岛有一个权值。这条回路的权值包括三个部分的和:1.经过的每个岛屿的权值;2.经过的连续两个岛屿的权值的乘积;3.能够构成三角型的连续三个岛的权值的乘积。 求路径的最大权值,以及最大权值有多少条。
【输入格式】
第一行,两个整数n(1≤n≤13)和m,分别表示岛的数量和桥梁的数量。 接下来一行,n个不超过100的整数,依次表示n个岛的权值。岛分别编号为1到n。 接下来m行,每行两个整数x和y,表示岛x和岛y之间有一座双向桥,可能存在重边。
【输出格式】
共一行,2个整数,分别表示最大权值和最大权值的数量。若无解,则输出0 0。 注意:当某条路径翻转后和其他路径相同,则认为这两天路径是重复的。如路径1234和路径4321是同一条路径。
【输入样例1】
3 3 2 2 2 1 2 2 3 3 1
【输出样例1】
22 3
【输入样例2】
4 61 2 3 41 2
1 3
1 42 3
2 43 4
【输出样例2】
69 1
【样例2解释】
构成的汉密尔顿路为1342。 第1部分:1+3+4+2=10; 第2部分:1*3+3*4+4*2=23; 第3部分1*3*4+3*4*2=36。 路径的权值和为10+23+36=69。
【解析】发现每个点的状态由前面两个点确定,用DP(S,A,B)表示状态为S时,当前到达A,而上一个点是B时的最大得分,这个状态由DP(S’,B,C)通过从B走到A得到,S’=S-(1<<A),即S’状态就是经过B和C但不经过A的一个状态,C是不同于A和B的一个点。
【状态转移】dp[S][A][B] =max(dp[S][A][B],dp[S’][B][C]+temp) 这里的temp指的是加上的得分即Vb*Va+Va,如果构成三角关系(即A和C间有边),temp就要再加上Vb*Va*Vc.
【边界条件】DP((1<<A)+(1<<B),A,B)=Va+Vb+Va*Vb(A和B间有边)表示
【代码】
#include <cstdio> #include <cstring> using namespace std; const int MAXN = 13; const int MAX_S = 1<<(MAXN+1); long long dp[MAX_S][MAXN+1][MAXN+1]; long long way[MAX_S][MAXN+1][MAXN+1]; int edge[MAXN+1][MAXN+1]; long long V[MAXN+1]; int N,M; int main(){ int cas; scanf("%d",&cas); while(cas --){ memset(edge,0,sizeof(edge)); scanf("%d%d",&N,&M); for(int i = 1; i <= N; ++i) scanf("%d",&V[i]); if(N == 1){ printf("%d 1\n",V[1]); continue; } int a,b; while(M --){ scanf("%d%d",&a,&b); edge[a][b] = edge[b][a] = 1; } memset(dp,-1,sizeof(dp)); memset(way,0,sizeof(way)); int ii,jj; long long temp; for(int i = 1; i <= N; ++i) for(int j = 1; j <= N; ++j){ if(i == j || !edge[i][j])continue; ii = 1<<(i-1); jj = 1<<(j-1); temp = V[i]+V[j]+V[i]*V[j]; dp[ii+jj][i][j] = temp; way[ii+jj][i][j] = 1; } for(int S = 0; S < (1<<N); ++S) for(int i = 1; i <= N; ++i){ if((S&(1<<(i-1))) == 0)continue; for(int j = 1; j <= N; ++j){ if((S&(1<<(j-1))) ==0 || i == j || !edge[i][j])continue; for(int k = 1; k <= N; ++k){ if(i == k || j == k ||(S&(1<<(k-1))) == 0)continue; int newS = S -(1<<(i-1)); if(dp[newS][j][k] ==-1)continue; if(!edge[j][k])continue; temp =V[i]+V[i]*V[j]+dp[newS][j][k]; if(edge[i][k])temp +=V[i]*V[j]*V[k]; if(dp[S][i][j] < temp){ dp[S][i][j] = temp; way[S][i][j] =way[newS][j][k]; //如果dp更新,way直接更新 } //如果dp不更新,但dp=temp,way加上原来的值 else if(temp ==dp[S][i][j])way[S][i][j] += way[newS][j][k]; } } } long long ans = -1,num = 0; int p = (1<<(N)) - 1; for (int i = 1; i <= N; ++i) for (int j = 1; j <= N; ++j){ if(i == j)continue; if (ans < dp[p][i][j]){ ans = dp[p][i][j]; num = way[p][i][j]; } else if (ans == dp[p][i][j]) num += way[p][i][j]; } if(ans == -1){ puts("0 0"); continue; } printf("%lld %lld\n",ans,num/2); } return 0; }
【ZOJ4257】MostPowerful
【在线测试提交传送门】
【问题描述】
火星上发现有有n中种气体,两两之间相互碰撞可以产生一定的能量,被碰撞的气体将消失。如a碰撞b,那么b气体就消失,并产生一定的能量。自身不能碰撞自身,问最后所能得到的最大能量。
【输入格式】
输入包含多组测试数据,对于每组测试数据: 第一行,一个正整数n (2 ≤ N ≤ 10),表示有n中气体; 接下来n行,每行n个整数Aij,表示气体i碰撞气体j时产生的能量,不超过1000。 最后一行用一个0,表示输入的结束。
【输出格式】
输出包含若干行,对于每组测试数据输出一行一个整数,表示最大能量。
【输入样例1】
2 0 4 1 0 3 0 20 1 12 0 1 1 10 0 0
【输出样例1】
4 22
【题目解析】用10位二进制表示气体是否存在,0表示存在,1表示不存在,S(上一个状态)中的两种气体碰撞并且有一种消失,可以得到newS的状态(状态转移)
【状态表示】dp[state] 状态为state时的最大能量
【转移方程】dp[state] = max(dp[state],dp[state’]+a[i][j])
【边界条件】dp[i] = 0;
【代码】
#include <cstdio> #include <cstring> using namespace std; #define max(a,b) (a) > (b) ? (a) : (b) const int MAXN = 10; const int MAX_S = 1 << 10; int a[MAXN+1][MAXN+1]; int dp[MAX_S]; int N; int main(){ while(scanf("%d",&N) != EOF){ if(N == 0)break; for(int i = 0; i < N; ++i) for(int j = 0; j < N; ++j){ scanf("%d",&a[i][j]); } memset(dp,0,sizeof(dp)); int full = 1 << N; for(int s = 0; s < full; ++s){ for(int i = 0; i < N; ++i){ if((s&(1<<i)))continue; for(int j = 0; j < N; ++j){ if(i == j)continue; if( (s&(1<<j)) )continue; int newS = s + (1<<j); dp[newS] = max(dp[newS],dp[s] + a[i][j]); } } } int ans = 0; for(int s = 0; s < full ; ++s) ans = max(ans,dp[s]); printf("%d\n",ans); } return 0; }
【POJ2411】Mondriaan’sDream
【在线测试提交传送门】
【问题描述】
给定一个h行w列的大矩形,使用1*2的小矩形填充。请计算一共有多少种完全覆盖的填充方案。
上图中2*4的大矩形有5种填充方案,2*3的大矩形有3中填充方案。
【输入格式】
输入包含多组测试数据,对于每组测试数据: 包含两个整数表示大矩形的长和宽,范围均为[1,11]。 最后一行两个0,表示输入的结束。
【输出格式】
输出包含若干行,对于每组测试数据依次输出一行一个整数,表示方案数。
【输入样例1】
1 2 1 3 1 4 2 2 2 3 2 4 2 11 4 11 0 0
【输出样例1】
1 0 1 2 3 5 144 51205
【解析】如果是横着的就定义11,如果竖着的定义为竖着的01,这样按行dp只需要考虑两件事儿,当前行&上一行,是不是全为1,不是说明竖着有空(不可能出现竖着的00),另一个要检查当前行里有没有横放的,但为奇数的1。
【状态表示】dp[state][i]第i行状态为state时候的方案数
【转移方程】dp[state][i] += dp[state’][i-1] state’为i-1行的,不与i行状态state冲突的状态
【边界条件】第一行 符合条件的状态记为1 即dp[state][0] = 1;
【代码】
#include <cstdio> #include <cstring> using namespace std; int M,N; long long dp[1<<11][11]; bool kexing[1<<11]; int full; inline bool check(int in) { int bit=0; while(in>0){ if( (in&1)==1) bit++; else{ if( (bit&1)==1) return false; bit=0; } in>>=1; } if( (bit&1)==1) return false; return true; } inline bool check2(int x1,int x2){ if( (x1|x2)!= full-1 ) //如果这里不用位运算,时间很长,要用约3000MS return false; return kexing[x1&x2]; } int main(){ full = 1<<11; for(int s = 0; s < full; ++s)if(check(s))kexing[s] = 1; while(scanf("%d%d",&M,&N) != EOF){ if(N == 0 && M == 0)break; memset(dp,0,sizeof(dp)); full = 1<<N; for(int s = 0; s < full; ++s){ if(kexing[s]) dp[s][0] = 1; } for(int i = 1; i < M; ++i){ for(int s = 0; s < full; ++s){ for(int s1 = 0; s1 < full; ++s1){ if(!check2(s1,s))continue; //if(dp[s1][i-1] == 0)continue; /*这一步判断的时间要大于位运算和+=的时间,如果先把这一步放在 check2前面,1300多MS,如果放在check2后面,610多MS,如果不加这 一步,560MS,但如果check2用的不是位运算,将这一步加在check2 面3000MS左右(水过),如果不加这一步,TLE */ dp[s][i] += dp[s1][i-1]; } } } int S = (1<<N) - 1; printf("%lld\n",dp[S][M-1]); } return 0; }
【HDU3681】PrisonBreak 状态压缩DP+BFS+二分答案
【在线测试提交传送门】
【问题描述】
在一个n*m的矩阵,每个格子的的情况可能是以下5种里的一种: (1)安全区,用字母‘S’表示; (2)起点,用字母‘F’表; (3)能量池,用字母‘G’表示。当机器人进入能量池格子,可以进行一次充电,机器人电量充满,能量池能量耗尽,变为一个空区。允许机器人再次进入,但不再充电。 (4)危险区,内含激光武器,用字母‘D’表示,机器人不能进入; (5)控制区,内含能量开关,用字母‘Y’表示,机器人进入将开关关闭。 机器人为了逃离矩阵,需要将所有的能量开关关闭。机器人移动到相邻的格子需要消耗1单位的能量,机器人能量耗尽时不能移动。 机器人的身上的电池有一定的容量,初始时能量是满的。请计算机器人的电池的最小容量是多少?
【输入格式】
第一行,两个整数n和m(1≤n,m≤15),表示矩阵的大小。 接下来n行,每行m个个大写字母,分别表示格子的情况。 能量池和能量开关的数量的和小于15个。
【输出格式】
对于每组测试数据,输出一行一个整数,表示机器人最小的电池容量,如果机器人不能逃出矩阵,输出-1。
【输入样例1】
5 5 GDDSS SSSFS SYGYS SGSYS SSYSS
【输出样例1】
4
【题目解析】机器人从出发点出发要求走过所有的Y,因为点很少,所以就能想到经典的TSP问题(起初我也想到了),但关于G点能充电的问题不知道怎么办?G点可以充电,到达G点就把当前能量更新为电池容量然后继续走。因为每个G点只能充一次电,这就好像TSP中的每个点只能走一次一样(G和Y都可以走多次,但走到G充电后,该点就变为了S,而走到Y关上开关以后,Y也变成了S。这是一个很巧妙地想法,所以要求Y点只能关一次开关,G点只能充一次电,这就是TSP了。),然后就是二分答案了,用状压DP判定当前电池容量的情况下是否能符合条件。
【状态表示】dp[s][i]表示到达当前i点状态为s时最大的剩余的能量
【转移方程】同TSP问题了
【边界条件】dp[1<<sid][sid] = rongliang.即出发点的能量就是电池容量.
【代码】
#include <cstdio> #include <cstring> #include <cmath> #include <queue> using namespace std; #define INF 0x1f1f1f1f int dp[32769][16]; int dist[16][16][16][16]; int di[4][2] = {{1,0},{-1,0},{0,1},{0,-1}}; int M,N,sid,nCnt,FinalState; char map[16][16]; struct node{ int x,y; node(){} node(int _x,int _y):x(_x),y(_y){} }nodes[16]; inline void BFS(node start) { queue<node> que; int sx = start.x,sy = start.y; dist[sx][sy][sx][sy] = 0; que.push(start); node cur; while(!que.empty()){ cur = que.front(); que.pop(); int x = cur.x,y = cur.y,tx,ty; for(int i = 0; i < 4; ++i){ tx = x + di[i][0]; ty = y + di[i][1]; if(tx < 0 || tx >= M || ty < 0 || ty >= N || map[tx][ty] == 'D')continue; if(dist[sx][sy][tx][ty] == -1){ dist[sx][sy][tx][ty] = dist[sx][sy][x][y] + 1; que.push(node(tx,ty)); } } } } inline bool ok(int s,int t){ //s状态中走过所有t状态中的城市 if(((s&t)&t) == t)return 1; return 0; } inline bool check(int step){ int res = -1; memset(dp,-1,sizeof(dp)); dp[1<<sid][sid] = step; int full = 1<<nCnt; for(int s = 0; s < full; ++s){ for(int i = 0; i < nCnt; ++i){ if((s&(1<<i)) == 0 || dp[s][i] == -1)continue; if(ok(s,FinalState))res = max(res,dp[s][i]); for(int j = 0; j < nCnt; ++j){ int temp = dist[nodes[i].x][nodes[i].y][nodes[j].x][nodes[j].y]; if(i == j || temp == -1 || (s&(1<<j)))continue; temp = dp[s][i] - temp; if(temp < 0)continue; int newS = s + (1<<j); dp[newS][j] = max(dp[newS][j],temp); if(map[nodes[j].x][nodes[j].y] == 'G')dp[newS][j] = step; } } } if(res < 0)return 0; return 1; } inline int solve(){ int low = 0,high = 300; int mid; while(low <= high){ mid = (low+high)/2; if(check(mid))high = mid-1; else low = mid+1; } if(low == 301)return -1; return low; } int main(){ while(scanf("%d%d",&M,&N) != EOF){ if(M == 0 && N == 0)break; nCnt = 0; FinalState = 0; for(int i = 0; i < M; ++i){ scanf(" %s",map[i]); for(int j = 0; j < N; ++j){ if(map[i][j] == 'F'){ sid = nCnt; nodes[nCnt++] = node(i,j); FinalState += (1<<sid); } else if(map[i][j] == 'G') nodes[nCnt++] = node(i,j); else if(map[i][j] == 'Y'){ int tid = nCnt; nodes[nCnt++] = node(i,j); FinalState += (1<<tid); } } } memset(dist,-1,sizeof(dist)) for(int i = 0; i < nCnt; ++i)BFS(nodes[i]); int ans = solve(); printf("%d\n",ans); } return 0; }
相关文章推荐
- 动态规划专题:树上DP和状态压缩DP
- BZOJ 1087 浅谈状态压缩动态规划的转移
- 动态规划之状态压缩
- POJ 1185 炮兵阵地(动态规划+状态压缩)
- 基于连通性状态压缩的动态规划--【插头DP】模板超级详细解释
- 男人8题之Tony's TourPKU1739基于连通性状态压缩的动态规划
- uva 11795 Mega Man's Mission(动态规划-状态压缩DP)
- kuangbin求带飞DP1 Doing HomeWork(动态规划+状态压缩)
- 炮兵阵地(poj1185,状态压缩的动态规划)
- 100道动态规划——27 POJ 1185 炮兵阵地 状态压缩,预处理,滚动数组
- 状态压缩动态规划 -- 棋盘问题 POJ 1321
- 状态压缩的动态规划
- 状态压缩动态规划 -- 炮兵阵地
- [34] Vijos P1002 过河(动态规划+状态压缩)
- UOJ 265 NOIP 2016 DAY2 T3 浅谈预处理状态压缩动态规划
- 基于连通性状态压缩的动态规划问题
- 多状态动态规划的压缩
- [Usaco2005 Open]Disease Manangement 疾病管理|状态压缩动态规划
- 状态压缩动态规划入门篇
- 状态压缩动态规划 -- 骨牌