演算法提高課 第一章 動態規劃③ 狀態壓縮DP
阿新 • • 發佈:2022-06-01
一、基於棋盤式(連通性)的狀態壓縮問題
1064. 小國王
#include<iostream> #include<cstring> #include<cstdio> #include<algorithm> #include<vector> using namespace std; typedef long long LL;//應該是溢位了對吧,所以我們要把所有的f改成long long const int N=12;//待會兒我會給大家解釋一下為什麼要開到12 const int M=1<<10,K=110;//K是我們的國王數量 int n,m;//這裡用m來表示國王數量,因為我習慣用n來表示一個數然後m來表示另外一個值 vector<int> state; //state 用來表示所有的合法的狀態 int id[M];//id的話是存這個每一個狀態和這個它的下標之間的對映關係 id有用到嗎?好像沒有用到 //應該是我之前寫的時候思路不太一樣,想的時候可能,就是前面設計的思路和實際用到的思路可能會有一點區別 //所以這裡其實是不要加id i 的 vector<int> head[M];//這個是每一狀態所有可以轉移到的其他狀態 int cnt[M];/*然後cnt的話存的是每個狀態裡面 1 的個數,因為我們剛才我們的狀態轉移方程裡面, 其實有一個需要求這個每一個狀態裡面1的個數的一個過程對吧*/ LL f[N][K][M];//這就是我們剛才說的那個狀態表示 bool check(int state) { for(int i=0;i<n;i++) if((state >> i & 1)&&(state >> i + 1 & 1)) return false;//如果存在連續兩個1的話就不合法 return true;//否則的話就是合法的 } int count(int state)//這裡y總沒具體解釋,我補充一下,這裡就是計算某個數二進位制裡面1的個數 { int res=0; for(int i=0;i<n;i++)res+=state>>i&1; return res; } int main() { cin>>n>>m; //首先我們需要把所有合法狀態處理出來對吧,把它預處理一下 for(int i=0;i<1<<n;i++) if(check(i)) /*check函式是檢查一下我們當前狀態是不是合法,也就是檢查一下我們這個狀態裡面是不是存在連續的兩個1, 如果不存在的話就表示它是合法的*/ { state.push_back(i); id[i]=state.size()-1;//id存的是我們這個合法狀態對應的下標是多少 cnt[i]=count(i);//cnt的話存的是這個i裡面 1 的個數是多少 } //然後我們來看一下這個不同狀態之間的一個這個邊的關係 for(int i = 0;i< state.size();i ++ ) for(int j=0;j<state.size();j++) { //用a來表示第一個狀態,用b來表示第二個狀態 int a=state[i],b=state[j]; //這裡是建立一個不同狀態之間的轉移關係 //先預處理一下哪些狀態和哪些狀態之間可以轉移 //首先轉移的話是要滿足兩個條件對吧 //一個是這個 a 和 b 的交集必須要是空集,它必須是空集才可以,否則的話同一列的兩個國王會攻擊到 //並且的話它們的這個這個並集的話也是需要去滿足我們不能包含兩個相鄰的1的 if((a&b)==0&&check(a|b)) // head[a].push_back(b); //然後只要這個 b 是合法的,那麼我們就把 b 新增到 a 可以轉移到的狀態集合裡面去 //這裡y總第一次寫錯了,debug了 head[i].push_back(j); //這裡是debug過程 //這裡寫錯了,這裡應該是head[i].push_back(j); //因為咱麼這裡去做的時候用的是下標,不是一個狀態的值 } //好,那剩下的就是 DP 了對吧 f[0][0][0]=1; //最開始的時候,我們f[0][0][0]=1 /*什麼意思呢,就是說,前0行對吧,我們前0行已經擺完了,其實也就是一行也沒有擺的情況下, 那麼此時由於我們這個是在棋盤外面, 所以肯定每個國王都不能擺對吧,所以我們就只有0這個狀態時合法的,那麼這個狀態的方案數是1*/ //好,然後從前往後列舉每一種狀態 for(int i=1;i<=n+1;i++) for(int j=0;j<=m;j++)//j的話是從0到m對吧,m表示的是國王數量 for(int a=0;a<state.size();a++)//然後我們來列舉一下所有的狀態,a表示第i行的狀態 for(int b : head[a])//然後來列舉所有a能到的狀態 { //這裡要判斷一下 //首先要判斷的是 //求一下我們a裡面的一個1的個數對吧 int c=cnt[state[a]]; //好,然後如果說,呃,就我們的j必須要大於等於c對吧,j是必須要大於等於c的 //為什麼呢,因為我們這個表示我們當前這行擺放的國王數量一定要小於等於我們整個的上限對吧 if(j>=c)//如果數說滿足要求的話,那麼我們就可以轉移了 { f[i][j][a]+=f[i-1][j-c][b]; //轉移的話就是f[i][j][a]+=f[i-1][j-c][b],然後從b轉移過來 } } //好,那我們最終的答案是什麼呢? //我們的最終的答案應該是這個f[n][m][ ],然後最後一維可以直接去列舉對不對 //去列舉一下最後一維是從,就是所有合法狀態都是可以的,就最後一行它所有合法狀態都是可以的對不對 //那這裡的話我們可以偷個懶,不是偷個懶,我們可以有個小技巧,就是我們在列舉i的時候,列舉到n+1就可以了 //就是我們去算到第i+1行,假設我們的棋盤是一個n+1 * n的一個棋盤,多了一行 /*那麼我們最終算的時候 就需要輸出一個 f[n+1],就是第n+1行, 一共擺到了第n+1行,然後m,然後0,因為第n+1行一個都沒擺,對吧*/ cout<<f[n+1][m][0]<<endl; /*就是我們假設存在第n+1行,但是第n+1行完全沒有擺, 那這種情況裡面的所有方案其實就是等於這個這個我們只有n行的一個所有方案,對吧*/ /*那這樣列舉n+1的一個好處是我們最後不需要再迴圈列舉最後一行的狀態了, 就是我們這個f[n+1][m][0]已經在這個迴圈裡面被迴圈算出來了*/ //所以可以少一層迴圈 /*這裡的話就是為什麼我們一開始N要從12開始,對吧,首先我們要用到11這個下標對吧, 那其實11這個下標是需要開長度是12才可以*/ return 0; }
327. 玉米田
#include <iostream> #include <cstring> #include <algorithm> #include <vector> using namespace std; typedef long long LL; const int N = 15,M = 1<<12,mod = 1e8; int m,n; int g[N]; vector<int>v; //存放所有一行內的合法方案 vector<int>head[M];//存放所有合法方案的合法轉移 LL f[N][M];//f[i][j]:前i行土地中,最後一行狀態為j的所有方案個數 bool check(int state)//檢查一行內狀態的合法性 { for (int i = 0; i < n; i ++ ) { if(state>>i & 1 && state>>(i+1) & 1) return false;//相鄰不能種 } return true; } int main() { scanf("%d%d", &m, &n); for (int i = 1; i <= m; i ++ ) { int t; for (int j = 0; j < n; j ++ ) { scanf("%d", &t); g[i] += (t<<j); //欣賞:二進位制狀態壓縮土地狀況 } } for (int i = 0; i < 1<<n; i ++ ) //列舉所有方案,篩選一行內合法的方案 { if(check(i)) { v.push_back(i); } } for(int i = 0;i<v.size();i++)//列舉所有一行內合法方案,選出合法的狀態轉移 { for(int j = 0;j<v.size();j++) { int a = v[i],b = v[j]; if((a&b) == 0) //上下相鄰不能種 { head[i].push_back(j); } } } f[0][0] = 1;//一行也不選,一個也不種的方案唯一 for (int i = 1; i <= m+1; i ++ )//列舉行,m+1行方便算答案 { for(int a = 0;a<v.size();a++)//列舉所有合法狀態 { for(int b:head[a])//列舉該狀態的可轉移狀態 { if((g[i] & v[a])!=v[a]) continue; //注意:若當前行土地狀況不允許該種植狀態,不能種 f[i][a] = (f[i][a] + f[i-1][b]) % mod; } } } cout<<f[m+1][0]<<endl;//前m+1行,最後一行不種植方案數等價於前m行種植的所有方案個數 return 0; }
292. 炮兵陣地
解法1:滾動陣列
#include <iostream> #include <cstring> #include <algorithm> #include <vector> using namespace std; const int N = 110,M = 1<<11; vector<int>v; int g[N]; int f[N][M][M];//f[i][j][k]:考慮前i行,最後一行狀態為j,倒數第二行狀態為k的方案的炮兵個數的最大值 int cnt[M];//二進位制中1的數量 int n,m; int count(int state) //計算1的數量 { int res = 0; for(int i = 0;i<m;i++) res += (state>>i & 1); return res; } bool check(int state)//檢查一行內狀態的合法性 { for(int i = 0;i<m;i++)//即每行中的1之間距離必須大於2 { if((state>>i & 1) && ((state>>i+1 & 1)|(state>>i+2 & 1))) return false; } return true; } int main() { scanf("%d%d", &n, &m); for (int i = 1; i <= n; i ++ ) { char c; for (int j = 0; j < m; j ++ ) { cin>>c; if(c=='H') g[i] += (1<<m-1-j);//將地圖以二進位制表示讀取,1表示高原 } } for(int i = 0;i<(1<<m);i++) //處理所有一行內合法的狀態 { if(check(i)) { v.push_back(i); cnt[i] = count(i); } } for (int i = 1; i <= n+2; i ++ )//列舉行,到n+2,方便計算最大值 { for (int j = 0; j < v.size(); j ++ ) //列舉第i行的合法狀態 { for(int k = 0;k<v.size();k++)//列舉第i-1行的合法狀態 { for(int u = 0;u<v.size();u++)//列舉第i-2行的合法狀態 { int a = v[j],b = v[k],c = v[u]; if((a&b)|(b&c)|(a&c)) continue;//三行內不得有同列 if(g[i] & a) continue;//第i行的狀態必須符合地圖狀態 f[i & 1][j][k] = max(f[i & 1][j][k],f[i-1 & 1][k][u] + cnt[a]); //將i-1行最大值加第i行擺放個數進行比較,選取較大者 } } } } cout<<f[n+2 & 1][0][0]<<endl;//滾動陣列優化:因為每次只同時用到2個狀態,減低空間複雜度 return 0; }
合法轉移預處理+滾動陣列優化
#include <iostream>
#include <cstring>
#include <algorithm>
#include <vector>
using namespace std;
const int N = 110,M = 1<<11;
int f[2][M][M];
int n,m;
int g[N];
int cnt[M];
vector<int>v;
vector<int>head[M];
bool check(int state)
{
for(int i = 0;i<m;i++)
{
if((state>>i & 1) && ( (state>>i+1 & 1) || (state>>i+2 & 1) ) ) return false;
}
return true;
}
int count(int state)
{
int res = 0;
for(int i = 0;i<m;i++)
{
res += (state>>i & 1);
}
return res;
}
int main()
{
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i ++ )
{
char c;
for (int j = 0; j < m; j ++ )
{
cin>>c;
if(c=='H') g[i] += (1<<j);
}
}
for (int i = 0; i < 1<<m; i ++ )
{
if(check(i))
{
v.push_back(i);
cnt[i] = count(i);
}
}
for (int i = 0; i < v.size(); i ++ ) //列舉兩行間的合法轉移
{
for(int j = 0;j<v.size();j++)//
{
int a = v[i],b = v[j];
if(!(a&b)) head[a].push_back(b);
}
}
for (int i = 1; i <= n + 2; i ++ ) //列舉第i行
{
for(int j = 0;j<v.size();j++) //列舉合法狀態
{
if(v[j] & g[i]) continue;
for(int k:head[v[j]])//列舉該狀態可合法到達的所有狀態,作為i-1行
{
for(int u:head[k])//列舉該狀態可合法到達的所有狀態,作為i-2行
{
if(u & v[j]) continue;//不能放高原處
f[i & 1][v[j]][k] = max(f[i & 1][v[j]][k],f[i-1 & 1][k][u] + cnt[v[j]]);
}
}
}
}
cout<<f[n+2 & 1][0][0]<<endl;//滾動陣列優化空間+n+2技巧
return 0;
}
二、基於集合的狀態壓縮DP
524. 憤怒的小鳥
#include <cstring>
#include <iostream>
#include <algorithm>
#include <cmath>
#define x first
#define y second
using namespace std;
typedef pair<double, double> PDD;
const int N = 18, M = 1 << 18;
const double eps = 1e-8;
int n, m;
PDD q[N];
int path[N][N];
int f[M];
int cmp(double x, double y)
{
if (fabs(x - y) < eps) return 0;
if (x < y) return -1;
return 1;
}
int main()
{
int T;
cin >> T;
while (T -- )
{
cin >> n >> m;
for (int i = 0; i < n; i ++ ) cin >> q[i].x >> q[i].y;
memset(path, 0, sizeof path);
for (int i = 0; i < n; i ++ )
{
path[i][i] = 1 << i;
for (int j = 0; j < n; j ++ )
{
double x1 = q[i].x, y1 = q[i].y;
double x2 = q[j].x, y2 = q[j].y;
if (!cmp(x1, x2)) continue;
double a = (y1 / x1 - y2 / x2) / (x1 - x2);
double b = y1 / x1 - a * x1;
if (cmp(a, 0) >= 0) continue;
int state = 0;
for (int k = 0; k < n; k ++ )
{
double x = q[k].x, y = q[k].y;
if (!cmp(a * x * x + b * x, y)) state += 1 << k;
}
path[i][j] = state;
}
}
memset(f, 0x3f, sizeof f);
f[0] = 0;
for (int i = 0; i + 1 < 1 << n; i ++ )
{
int x = 0;
for (int j = 0; j < n; j ++ )
if (!(i >> j & 1))
{
x = j;
break;
}
for (int j = 0; j < n; j ++ )
f[i | path[x][j]] = min(f[i | path[x][j]], f[i] + 1);
}
cout << f[(1 << n) - 1] << endl;
}
return 0;
}