狀態壓縮DP入門(算入了個門吧)
感謝博主無私分享
狀態壓縮鼻祖: n皇后問題
描述:
傳統做法是對每一行 i,暴力查詢每一列 j 。 主要浪費時間在於檢查是否有皇后會攻擊,又花費o(n)。因此一共是o(n^3)。
改進演算法引入狀態壓縮,依舊是每行i , 每列j 跑不掉的 o(n^2) ,不過判斷上面可以用位運算花費o(1),實現複雜度降低
那麼位運算是如何用於判斷某個位置上是否收到攻擊的呢? 舉個例子吧
第1行放置的情況 1 0 0 0
第2 0 0 1 0
結合前兩行,第3行在這些位置上在豎直方向上已經不能再放了( 約束條件 ) 1 0 1 0。按照這個思想,我們再算出主對角線上的約束條件,副對角線上的約束條件,通過總約束條件的約束,剩下的點就是可以放皇后的合法位置, 我們依次dfs這些合法位置就行。
所以現在問題就在於如何計算這些約束條件,這裡有篇很好的文章(再次感謝博主無私分享,上面是我看完自己的理解,可以移步去這個連結了)下面記錄轉載過來的位運算筆記用自己的話理解
在狀態壓縮中,通常用 ( 1 << N ) - 1 來表示最大狀態MAXST(全部都是1 ,11111111),用 A | B 來合併A和B的狀態位,用 A & ~B 來清除A狀態中所有的B中的狀態位。
一、DFS函式引數Col,MainDiag,ContDiag的表示意義:
上面講的,前 i行上的總列約束條件、主對角線約束、副對角線約束
二、Col == ( 1 << N ) - 1 的表示意義:
前i行把所有列上的總約束條件佔滿了 ,那麼就是全擺上1了啊,也就是說找到了一種方案
三、emptycol = ( ( 1 << N ) - 1 ) & ~( Col | MainDiag | ContDiag ) 的表示意義:
111111 & (~ 00010) 則得到 1111101 , 相當於扣掉了總約束條件,剩下的合法序列
四、curcol = ( emptycol ) & ( ( ~emptycol ) + 1 ) 的表示意義:
也就是 lowbit = x & (-x) ,補碼等於反碼+1 , 比如說x等於 110 則 -x : 010 , 因此lowbit = 010,得到最右邊的合法位置。 也就是用於遍歷合法序列
五、 emptycol &= ~curcol 的表示意義:
類似於三,當前選中了curcol, 那麼合法序列就扣掉當前位置
六、DFS遞迴的函式引數 Col | curcol,( MainDiag | curcol ) >> 1,( ContDiag | curcol ) << 1 的表示意義:
第一個引數在上面已經講過了,現在講第二個引數, 我們還是分析第1行對第2行的影響。
例如: 1 -> 0 1 0 0 0
第二行怎麼找到自己主對角線上的 約束呢 ,其實只要把 第一行右移一位,然後照著第一個引數的方法累加,得到的就是對第二行的約束。 因為 (x,y) 的主對角線約束是 (x-1, y-1)。
第三個引數也就類似
以上都是本焫雞通過自己能理解的方式理解的,要是有什麼不對的地方請指出
程式碼:
// 位運算
#include<stdio.h>
#include <iostream>
using namespace std;
int n,high,ans;
// col 是前i-1行擺放的皇后 聯合起來對 第i行的約束
// fir 前i-1行 擺放的皇后,在主對角線方向上,對i行的約束
// sec 是前i-1行擺放的皇后,在負對角線上對第i行的約束
void dfs(int col,int fir,int sec)
{
if(col == high)
{
ans++;
return;
}
int cur = high & (~(col|fir|sec)); //11111的情況 剔除掉 三座大山聯合約束的情況 ,得到的序列, 存在某位為1 表示該位可以放
while(cur)
{
int lowbit = cur &(-cur); //得到最後一個位置
dfs(col|lowbit, (fir|lowbit)>>1 , (sec|lowbit)<<1);
cur = cur & (~lowbit); // 最後一個1 標記為1 。 配合while迴圈。相當於把所以可能性遍歷了一遍
}
}
int main()
{
while(~scanf("%d",&n)&&n)
{
ans=0;
high = (1<<n)-1;
dfs(0,0,0);
printf("%d\n",ans);
}
return 0;
}
求到當前所用的列是第幾位,然後帶回去計算值,記憶話搜尋剪枝
程式碼:
#include <iostream>
#include <bits/stdc++.h>
using namespace std;
const int maxn = 100;
int mp[maxn][maxn], dp[maxn][maxn];
int n,ans, high;
int dfs(int col,int tmp, int i)
{
if(dp[col][i] != -1) return dp[col][i];
if(col == high)
{
return dp[col][i] = tmp;
}
int last = high& (~col);
int ans2 = 0;
while(last)
{
int lowbit = last & (-last); // 得到最低位的1 的位置
int cnt = log2(lowbit)+1;
// cout<<cnt<<endl;
ans2 = max(tmp,dfs(col|lowbit, tmp + mp[i][n-cnt+1], i+1));
last = last & (~lowbit);
}
return dp[col][i] = ans2;
}
int main()
{
int t;
scanf("%d",&t);
while(t--)
{
memset(dp,-1,sizeof(dp));
ans = 0;
scanf("%d",&n);
high = (1<<n)-1;
for(int i=1; i<=n; i++)
{
for(int j=1; j<=n; j++)
{
scanf("%d",&mp[i][j]);
}
}
printf("%d\n",dfs(0,0,1));
}
return 0;
}
lightoj 1011 - Marriage Ceremonies
題意:
有N個怪獸,給出H[ ]陣列,代表每個怪獸的HP值,給出 mp[ n ][ p ],表示第n個怪獸死了之後,獲得它的裝備可以對 第p 個怪獸造成的傷害( 每攻擊一下 ), 另外,我們還有一把小手槍 每攻擊一次 造成 1 傷害,問的是至少攻擊幾次能把所有怪獸殺死?
思路:
如果用狀態壓縮儲存怪獸存亡狀態,那麼陣列只需要開 1<<16,我們考慮的是殺小怪獸的順序,和使用哪把武器( 這裡無限使用 ),因此我們暴力殺怪獸的順序,n ! 級別, 每殺一隻怪獸都使用效果最好的武器( 貪心 ),因此搜尋的結構就是(搜尋第一步一定要確立好結構,之後再填上細節,剪枝)
int dfs(int state)
{
for(怪獸:) //選出還活著的
{
for( 武器: ) // 貪心找出效果最好的
{
cos = min ;
}
DP[state] = max(DP[state], dfs(殺完小怪獸的state) + cos);
}
}
main{
dfs(0);
}
我們可以用1 表示怪獸已經死了,這裡使用的不是之前的位運算遍歷, 因為狀態的0 我們也需要考慮, 那麼如果僅僅接受狀態,而不接收n , 例如 state = 1 , 那麼我們無法知道還活著的還有幾隻,也就是1前面有幾個
程式碼:
#include <iostream>
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1000;
const int inf = 0x3f3f3f3f;
int mp[maxn][maxn], dp[maxn], h[maxn];
char st[maxn];
int n,ans, high;
int dfs(int state)
{
if(dp[state] != inf)
return dp[state];
if(state == high)
return dp[state] = 0;
for(int i=1; i<=n; i++) // 選擇殺哪個怪物
{
if( !(state& (1<< (i-1) ) ) ) // 找出活的
{
// cout<<"I am alive"<<endl;
int cnt = inf;
for(int j=1; j<=n; j++) // 用哪個槍
{
if(state & (1<< (j-1) ) && mp[j][i] != 0) // 死了的為1 , 那麼& 真, 就是能用這把槍
{
// cout<<"you are my gun"<<endl;
int cos = h[i] / mp[j][i] + ((h[i] % mp[j][i])? 1:0) ;
cnt = min(cnt, cos);
}
}
if(cnt == inf)
{
cnt = h[i];
}
dp[state] = min(dp[state], dfs(state | ( (1<<i-1) )) + cnt);
}
}
return dp[state] ;
}
int main()
{
int t;
scanf("%d",&t);
while(t--)
{
// memset(dp,-1,sizeof(dp));
ans = 0;
scanf("%d",&n);
for(int i=1; i<=n; i++)
scanf("%d",&h[i]);
high = (1<<n) -1;
for(int i=0; i<= high ; i++)
dp[i] = inf;
for(int i=1; i<=n; i++)
{
scanf("%s",st);
for(int j=0; j<n; j++)
{
mp[i][j+1] = st[j]-'0';
}
}
printf("%d\n",dfs(0));
}
return 0;
}
Poj - 3254
題目大意:農夫有一塊地,被劃分為m行n列大小相等的格子,其中一些格子是可以放牧的(用1標記),農夫可以在這些格子裡放牛,其他格子則不能放牛(用0標記),並且要求不可以使相鄰格子都有牛。現在輸入資料給出這塊地的大小及可否放牧的情況,求該農夫有多少种放牧方案可以選擇(注意:任何格子都不放也是一種選擇,不要忘記考慮!
Sample Input
2 3
1 1 1
0 1 0
Sample Output
9
分析
本題中,假設第i行允許 j 种放牛方式(即j種狀態) ,那麼對於每一種狀態 k (k (= [1, j]),把上一行 i-1 上滿足和 k 不衝突的(題目要求上下不能同時為1 )狀態累加,相當於每個k對應一系列 i-1 行滿足的狀態 , 遍歷 k 累加就行,直接上程式碼讀起來容易些。
該型別題目由於狀態很小,所以每次暴力查詢狀態。
程式碼:
#include <cstdio>
#include <cstring>
using namespace std;
#define mod 100000000
const int maxn = 10050;
int M,N,top = 0;
//top表示每行最多的狀態數
int state[600],num[110];
//state存放每行所有的可行狀態(即沒有相鄰的狀態
//
int dp[20][600];
//dp[i][j]:對於前i行資料,每行有前j種可能狀態時的解
int cur[20];
//cur[i]表示的是第i行整行的情況
inline bool ok(int x) //判斷狀態x是否可行
{
if(x&x<<1)
return false;//若存在相鄰兩個格子都為1,則該狀態不可行
return true;
}
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) //判斷狀態x 與第k行的實際狀態的逆是否有‘重合’
{
if(x&cur[k])
return false; //若有重合,(即x不符合要求)
return true; //若沒有,則可行
}
int main()
{
while(scanf("%d%d",&M,&N)!= EOF)
{
init(); // 1 - 1<<N 的所以可行狀態儲存在state[1-top]中
memset(dp,0,sizeof(dp));
for(int i = 1; i <= M; ++i)
{
cur[i] = 0; // cur[i] 代表第i行草地的情況 1001 還是 0100等等
int num;
for(int j = 1; j <= N; ++j) //輸入時就要按位來儲存,cur[i]表示的是第i行整行的情況,每次改變該數字的二進位制表示的一位
{
scanf("%d",&num); //表示第i行第j列的情況(0或1)
if(num == 0) //若該格為0
cur[i] +=(1<<(N-j)); //則將該位置為1(注意要以相反方式儲存,即1表示不可放牧
// 假設輸入 1010 那麼cur[i]為 1010 , 第一個讀入的 左移 4-1 次
}
}
for(int i = 1; i <= top; i++)
{
if(fit(state[i],1)) //判斷所有可能狀態與第一行的實際狀態的逆是否有重合
{
dp[1][i] = 1; //若第1行的狀態與第i種可行狀態吻合,則dp[1][i]記為1
}
// 例如第一行本應該是 111 那麼轉換成了 000 , 則狀態101 跟第一行fit為1 ,則dp[1][state - 101] = 1
}
/*
狀態轉移過程中,dp[i][k] =Sigma dp[i-1][j] (j為符合條件的所有狀態)
dp[i][k] = Sigma dp[i-1][j] (j為滿足條件的所有狀態)
*/
for(int i = 2; i <= M; ++i) //i索引第2行到第M行
{
for(int k = 1; k <= top; ++k) //該迴圈針對所有可能的狀態,找出一組與第i行相符的state[k]
{
if(!fit(state[k],i))
continue; //判斷是否符合第i行實際情況
for(int j = 1; j <= top ; ++j) //找到state[k]後,再找一組與第i-1行符合,且與第i行(state[])不衝突的狀態state[j]
{
if(!fit(state[j],i-1))
continue; //判斷是否符合第i-1行實際情況
if(state[k]&state[j])
continue; //判斷是否與第i行衝突
dp[i][k] = (dp[i][k] +dp[i-1][j])%mod; //若以上皆可通過,則將'j'累加到‘k'上
// dp[i][k] = dp[i][k] + d[i-1][j] 第i行的狀態 加上i-1行可用的狀態。
// 比如說 i行有兩種狀態 010 000 那麼i就要考慮這兩種情況, 當選擇第一個010 時, i-1 有四種情況,也就是j有四種
}
}
}
int ans = 0;
for(int i = 1; i <= top; ++i) //累加最後一行所有可能狀態的值,即得最終結果!!!泥馬寫註釋累死我了終於寫完了!
{
ans = (ans + dp[M][i])%mod;
}
printf("%d\n",ans);
}
}
棋盤放子
有一個N*M(N<=5,M<=1000)的棋盤,現在有1*2及2*1的小木塊無數個,要蓋滿整個棋盤,有多少種方式?答案只需要mod1,000,000,007即可。
例如:對於一個2*2的棋盤,有兩種方法,一種是使用2個1*2的,一種是使用2個2*1的。
由於行數n很小,那麼我們每一列的狀態最多隻有 1<<5 種,做法就是針對每一列,搜尋出所有合法狀態。
當前列僅僅影響下一列,比如說第一列放了1*2 ,那麼第二列該位置就不能再放。
狀態轉移方程 dp[ j+1 ][ nex ] += dp[ j ][ state ] , nex是由當前state限制下能構成的合法狀態
程式碼:
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<iostream>
#include<string>
#include<vector>
#include<stack>
#include<bitset>
#include<cstdlib>
#include<cmath>
#include<set>
#include<list>
#include<deque>
#include<map>
#include<queue>
using namespace std;
typedef long long ll;
const double PI = acos(-1.0);
const double eps = 1e-6;
const int INF = 0x3f3f3f3f;
const int maxn = 100;
const int mod = 1e9 + 7;
int T, n, m;
ll dp[11][(1 << 11) + 5];
void dfs(int i,int j,int state, int nex)
// 第幾行,第幾列,當前狀態,對下一行的影響
{
if(i == n) // 到達第n+1 行的時候,當前列已經得到一種狀態 state,且產生對下一行允許的 nex
{
dp[j+1][nex] += dp[j][state];
return;
}
if( (1<<(i) &state) > 0) // 當前行 被佔用了,那麼就跳到下一行
dfs(i+1,j,state,nex);
else
{
dfs(i+1,j,state, nex|(1<<i));//在當前行擺上 1*2,則下一列被佔用一個
if(i+1<n && (((1<<(i+1)) & state) == 0) ) // 在當前列擺上2*1
{
dfs(i+2,j,state,nex);
}
}
}
int main()
{
while(~scanf("%d%d",&n,&m))
{
if(m == 0 && n == 0) break;
memset(dp,0,sizeof(dp));
dp[1][0] = 1;
for(int j=1; j<=m; j++)
{
// 第j列
for(int k=0; k<(1<<n); k++)
// 各個狀態
{
if(dp[j][k])//dp[j][k]!=0 表示第j行 k狀態下,有方案可以放置木板,
dfs(0,j,k,0);
}
}
printf("%lld\n",dp[m+1][0]);
}// 要求鋪滿,那麼m+1 不能有露出來的}
return 0;
}