动态规划-状压DP
状态压缩DP,也就是把局面状态压缩到一个整数中,用整数的运算表示状态转移
模板题一般是,往棋盘上放同一种棋子,并且不能让他们掐架,问题有两种:
一共要放k个,求放法有几种?
最多放多少个?
g++ -std=c++11 -O2
一个\(N\times N\) 的国象棋盘上放\(k\) 个王八,要求这k个王八不能掐架.求有多少种放王八的方法
其中\(1\leq N\leq 9,0\leq k\leq N\times
N\)
类似于八皇后问题,但是王八的攻击范围只有周围八格,更近
状态压缩
棋盘的每个格子,要么有王八,要么没王八,因此
可以用1表示有,0表示无的状态
那么整个棋盘可以看成有N行,每行有N个格子
比如一个\(4\times
4\) ,没有王的棋盘就可以是
1 2 3 4 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
可以将一行压缩为一个整数,最左侧是最低位比如
1 0 0 1 0 1 0 ->0101001b=41
如果想让一行中的王八不掐架,那么不能有任何两个1相邻.这一点怎么判断呢?
假设current_state=41表示当前行状态
那么(current_state&((current_state>>1)|current_state<<1))
就可以判断有没有王八掐架
为啥呢?
比如状态41显然是合法的
状态
整数
二进制
current_state
41
0101001
current_state<<1
1010010
current_state>>1
0010100
(current_state<<1)|(current_state>>1)
1010110
current_state
0101001
(current_state&((current_state>>1)|(current_state<<1))
0000000
如何让相邻两行的王八不掐架呢?
(current_state&((previous_state)|(previous_state<<1)|(previous_state>>1)))
道理类似于不让一行中的王八掐架,上一行的一个王八,那么本行中该王八的左下,正下,右下都不能有王八
状态转移
设计状态:
dp[currnet_row][cnt_king_used][current_state]
表示:
已经安排了[1,current_row]
的王八,
已经安排了cnt_king_used
个王八,
当前行,也就是第current_row
的状态是current_state
状态转移方程: \[
\begin{aligned}
&dp[current\_row][cnt\_king\_used][current\_state]=\\
&\sum_{previous\_state}dp[previous\_row][cnt\_kings\_used-cnt\_kings\_of[current\_row]\
][previous\_state]
\end{aligned}
\] 初始状态:
前0行(不存在这一行),放了0个王八,状态显然是0,此时的摆放方法dp[0][0][0]=0
完整程序
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 #include <iostream> #include <vector> #include <algorithm> using namespace std;long long dp[10 ][100 ][2000 ];int n,k;vector<int >legal_states; int cnt_kings_in_state[2000 ];bool isLegalState (const int &k) { if ((k&((k<<1 )|(k>>1 )))==0 ){ return true ; } return false ; } bool isInvasion (const int &a,const int &b) { return (a&((b<<1 )|(b>>1 )|b)); } int main () { cin>>n>>k; int current_state; int cnt_state; for (int i=0 ;i<(1 <<n);i++){ current_state=i; cnt_state=0 ; while (current_state){ cnt_state+=current_state&1 ; current_state>>=1 ; } cnt_kings_in_state[i]=cnt_state; if (isLegalState (i)){ legal_states.push_back (i); } } dp[0 ][0 ][0 ]=1 ; for (int current_row=1 ;current_row<=n;current_row++){ auto previous_row=current_row-1 ; for (auto current_state:legal_states){ for (auto previous_state:legal_states){ if (isInvasion (current_state,previous_state)==false ){ for ( int cnt_tot_kings=cnt_kings_in_state[current_state]; cnt_tot_kings<=k; ++cnt_tot_kings ){ dp[current_row][cnt_tot_kings][current_state]+= dp[previous_row][cnt_tot_kings-cnt_kings_in_state[current_state]][previous_state]; } } } } } long long result=0 ; for (auto current_state:legal_states){ result+=dp[n][k][current_state]; } cout<<result<<endl; return 0 ; }
\(N\times
N\) 的棋盘上要放N的皇后,要求N个皇后不能掐架,求多少种放皇后的方法
\(6\leq N\leq 13\)
状态压缩
N行,每行N位,每位0表示没有皇后,1表示有皇后
一个int有32位,足够表示一行
每行的最左侧格子是int的最低位
状态转移
设计状态
\(dfs(col\_state,diagonal\_state,rdiagonal\_state,row)\)
表示当前要放第row行的皇后,
之前所有的皇后控制的所有列存放在col_state
中,
之前所有皇后控制的左上到右下的斜线在本行的影响 存放在diagonal_state
中
之前所有皇后控制的右上到左下的斜线在本行的影响存放在人diagonal_state
中
显然\(dfs(col\_state,diagonal\_state,rdiagonal\_state,row)\) 的任务就是,结合前三个参数,在本行找一个安全位置放一个皇后,然后进入下一层dfs
啥意思呢?8
假如现在要放第四行,前三行的皇后如图所示
image-20221222114520664
这三个皇后控制的列对第四行的影响如图
image-20221222114303317
那么此时第四行的2,4,6列就不允许放皇后,
此时col_state=00101010b
,(最左侧格子在最低位)表示第2,4,6列不允许使用
前三个皇后在左上到右下的斜线上,对第四行产生的影响如图
image-20221222114701433
即此时diagonal_state=01110000
(最左侧格子在最低位),表示第5,6,7列不允许使用
rdiagonal_state
同理
下面考虑这三个表示占用状态的参数如何维护
显然col_state最容易维护,只要第i列上有过皇后,col_state的相应二进制位就置1
对于diagonal_state,假设本行的皇后放在第i列,那么他会影响下一行的i+1列,因此从当前行的dfs进入下一行的dfs时,只需要把diagonal_state加上本行的皇后列位置,然后右移1位传递给下层dfs
image-20221222150322875
综上,对于当前行的dfs,放置皇后时需要考虑的因素如图:
image-20221222150632385
状态转移方程采用"推"的方式,从row层推进到row+1层 \[
\begin{aligned}
&dfs(col\_state,diagonal\_state,rdiagonal\_state,row)\\ \Rightarrow
&dfs(col\_state|current\_state,(diagonal\_state|current\_state)>>1,(rdiagonal\_state|current\_state)<<1,row+1)
\end{aligned}
\]
完整代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 #include <iostream> #include <vector> #include <algorithm> using namespace std;int n;const int maxn=20 ;int log_2[10000 ];int cnt_success=0 ;int limit_state;int col[maxn];int lowbit (int &x) { return x&(-x); } void print () { for (int i=1 ;i<=n;++i){ cout<<col[i]+1 <<" " ; } cout<<endl; } void dfs (int column_state,int diagonal_state,int rdiagonal_state,int row) { if (row>n){ ++cnt_success; if (cnt_success<=3 ){ print (); } return ; } int capture_state=(column_state|diagonal_state|rdiagonal_state)&limit_state; int available_state=(~capture_state)&limit_state; int position_state; int position; while (available_state){ position_state=lowbit (available_state); position=log_2[position_state]; col[row]=position; dfs (column_state|position_state,(diagonal_state|position_state)>>1 ,(rdiagonal_state|position_state)<<1 ,row+1 ); available_state-=position_state; } } int main () { cin>>n; limit_state=(1 <<n)-1 ; log_2[1 ]=0 ; for (int i=2 ;i<10000 ;i++){ log_2[i]=log_2[i/2 ]+1 ; } dfs (0 ,0 ,0 ,1 ); cout<<cnt_success<<endl; return 0 ; }
\(M\times N\) 个格子的矩形地面
然后输入\(M\times
N\) 个数表明这个地面是否肥沃,1肥沃,0贫瘠
其中\(1\leq M,N\leq 12\)
肥沃的土地可以种草,贫瘠的不行
任何两个草地不能有相邻边,可以有相邻顶角
求有多少种安排方法
问题可以向八王八靠拢,如果种草地可以视为占了一个中国象棋的将军,前后左右四个格子内不允许有第二个将军.贫瘠的格子不允许放棋
如果八王八问题中也有一些棋盘不允许放置棋子则和本题就很相似了
状态压缩
一行最多有12个格子,每个格子要么种草,要么不中,一个二进制位两种状态表示一个格子即可,那么一行只需要12个二进制位,显然一个int有32位足够表示一行的状态
贫瘠的格子如何建模?贫瘠的格子不允许种草就行了呗
状态转移
类比八国王问题,设计dp状态 \[
dp[current\_row][current\_state]
\] 表示:
已经安排了[1,current_row]行,
其中第current_row行的状态是current_stata,
满足上述两个条件的不同安排数目是dp[current_row][current_state]
状态转移方程是 \[
dp[current\_row][current\_state]=\\
\sum_{previous\_state}dp[previous\_row][previous\_state]
\]
这里要求previous_state
和current_state
两个状态不能相互冲突,也就是上一行的种草格子,下一行这个格子不允许种草
完整代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 #include <iostream> #include <vector> #include <iomanip> #include <algorithm> using namespace std;int m,n;const int maxn=14 ;const int mod=1e9 ;int row_base_state[maxn];vector<int > row_legal_state[maxn]; int row_limit_state;int dp[maxn][1000000 ];int main () { cin>>m>>n; row_limit_state=(1 <<n)-1 ; int single_state; for (int i=1 ;i<=m;i++){ int row_state=0 ; for (int j=1 ;j<=n;j++){ cin>>single_state; row_state=row_state*2 +single_state; } row_base_state[i]=row_state; for (row_state=0 ;row_state<=row_limit_state;++row_state){ if ((row_state&((row_state<<1 )|(row_state>>1 )))==0 && ((row_state&((~row_base_state[i])&row_limit_state))==0 )){ row_legal_state[i].push_back (row_state); } } } dp[0 ][0 ]=1 ; row_legal_state[0 ].push_back (0 ); for (auto current_row=1 ;current_row<=m;++current_row){ auto previous_row=current_row-1 ; for (auto current_row_state:row_legal_state[current_row]){ for (auto previous_row_state:row_legal_state[previous_row]){ if ((current_row_state&previous_row_state)==0 ){ dp[current_row][current_row_state]+=dp[previous_row][previous_row_state]; } } } } int result=0 ; for (int i=0 ;i<=row_limit_state;i++){ result=(result+dp[m][i])%mod; } cout<<result<<endl; return 0 ; }
给一个P和H表示的地形地图
img
其中只有P(Plain,平原)格子可以放炮兵,H山地不行
状态压缩
一个炮兵的打炮范围是横竖的两格相当于中象里将军腿再长一点
实际上和P1879棒子地很相似了,P1879种草的方格相当于放了有一个中象的将军,这个题的格子相当于放了一个壮一点的将军
原来一个将军顶多影响跟前的四个格子,现在一个炮可以影响8个格子,
不妨把跟前这四个格子叫做"一级影响",稍微远一点的四个格子叫做"二级影响",如下图示意
image-20221222194252888
可见一个将军和一个炮的区别就是,将军只有一级影响,炮还有二级影响
P1879这个题中要安排一行时,只需要考虑上一行给这一行造成的一级影响.
本题中除了一级影响,还要考虑二级影响,因此本题可以看作P1879的推广,
状态转移
原来的状态dp[current_row][current_state]
表示1到current_row
行已经安排完毕,其中current_state
是第current_row
行的状态,也就是对current_row+1
行的"一级影响"
因此拓展到有二级影响的情况,应该这样设置状态dp[current_row][current_state][previous_state]
,意思是
当前已经安排好了1到current_row
行,
第current_row
行的状态是current_state
,
第previous_row=current_row-1
行的状态是previous_state
dp[current_row][current_state][previous_state]
等于在前述三个条件下得到的最大炮兵数量
\[
当前状态的最大炮兵数=max(枚举与当前行不冲突的上一行状态的最大炮兵数+当前行状态贡献的炮兵数)
\]
状态转移方程就是:
1 2 3 dp[current_row][current_state][previous_state]=max ( dp[previous_row][previous_state][preprevious_state]+cnt_of_cannons_in_state[current_state]; )
完整代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 #include <iostream> #include <algorithm> #include <vector> using namespace std;const int maxn=110 ;int n,m;int row_limit_state;int row_base_state[maxn];vector<int > row_legal_states[maxn]; int cnt_max_cannons;int dp[maxn][1025 ][1025 ];int cnt_cannons_in_state[1025 ];int main () { cin>>n>>m; int single_state; char terrain; row_limit_state=(1 <<m)-1 ; for (auto row_state=0 ;row_state<=row_limit_state;++row_state){ int row_temp_state=row_state; int cnt_cannons=0 ; while (row_temp_state){ cnt_cannons+=row_temp_state&1 ; row_temp_state>>=1 ; } cnt_cannons_in_state[row_state]=cnt_cannons; } for (int row=1 ;row<=n;row++){ int row_state=0 ; for (int col=1 ;col<=m;++col){ cin>>terrain; if (terrain=='P' ){ single_state=1 ; } else { single_state=0 ; } row_state=row_state*2 +single_state; } row_base_state[row]=row_state; for (row_state=0 ;row_state<=row_limit_state;row_state++){ if ( (row_state&(((row_state<<1 )|(row_state<<2 )|(row_state>>1 )|(row_state>>2 ) )&row_limit_state) )==0 && (row_state&((~row_base_state[row])&row_limit_state))==0 ){ row_legal_states[row].push_back (row_state); } } } dp[0 ][0 ][0 ]=1 ; row_legal_states[0 ].push_back (0 ); for (auto current_state:row_legal_states[1 ]){ dp[1 ][current_state][0 ]=cnt_cannons_in_state[current_state]; cnt_max_cannons=max (cnt_max_cannons,dp[1 ][current_state][0 ]); } for (auto current_row=2 ;current_row<=n;++current_row){ auto previous_row=current_row-1 ; auto preprevious_row=previous_row-1 ; for (auto current_state:row_legal_states[current_row]){ for (auto previous_state:row_legal_states[previous_row]){ for (auto preprevious_state:row_legal_states[preprevious_row]){ if ( (previous_state & preprevious_state)==0 && (current_state&(previous_state|preprevious_state))==0 ){ dp[current_row][current_state][previous_state]=max ( dp[current_row][current_state][previous_state], dp[previous_row][previous_state][preprevious_state]+cnt_cannons_in_state[current_state] ); cnt_max_cannons=max (cnt_max_cannons,dp[current_row][current_state][previous_state]); } } } } } cout<<cnt_max_cannons<<endl; return 0 ; }
在一个\(n\times
m\) 的棋盘上,任意放中象的炮,要求任意两个炮不能相互攻击,随便放炮,问最多有多少种放法
啥时候能炮打炮?一条水平或者竖直线上有仨个以上的炮就要打炮了
因此就问题转化为,\(n\times
m\) 个方格,每行每列可以有0或者1或者2个石头,求有多少种摆放方法
状态压缩
我最初的想法是,每行,每列都记录已经放过几个石头,然后直接深搜
dfs(x,y)是当前要在第x行第y列放石头,首先判断能否放石头,如果能则尝试放上石头然后下一步深搜.
不管能不能放石头,都要有一个直接下一步深搜
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 #include <iostream> #include <algorithm> #include <vector> using namespace std;const int maxn=110 ;const int mod=9999973 ;int n,m;int cnt_on_col[maxn];int cnt_on_row[maxn];int cnt_result=0 ;void DFS (int y,int x) { if (y<=n&&x>m){ DFS (y+1 ,1 ); return ; } if (y>n){ cnt_result=(cnt_result+1 )%mod; return ; } if (cnt_on_col[x]<2 &&cnt_on_row[y]<2 ){ ++cnt_on_col[x]; ++cnt_on_row[y]; DFS (y,x+1 ); --cnt_on_col[x]; --cnt_on_row[y]; } DFS (y,x+1 ); } int main () { cin>>n>>m; DFS (1 ,1 ); cout<<cnt_result<<endl; return 0 ; }
结果就三个测试点过了,其他全都TLE
狗屁状压,dp[i][j][k]
表示前i行中的列,有一个炮的列有j个,有两个炮的列有k个
完事