1. 程式人生 > 其它 >DP專題-專項訓練:狀壓 DP

DP專題-專項訓練:狀壓 DP

目錄

一些 update

update on 2021/7/19:發現第二道題目的程式碼貼錯了,現已更正。

1. 前言

本篇博文是狀壓 DP 的練習題博文。

沒有學過狀壓 DP?

傳送門:演算法學習筆記:狀態壓縮 DP

狀壓 DP 非常之靈活,這裡選了 3 道經典題。

更多的題目?請前往洛谷使用者 @StudyingFather 的 一個動態更新的洛谷綜合題單 檢視。

2. 練習題

題單:

P2704 [NOI2001] 炮兵陣地

這道題是一道簡單題,相信各位掌握了互不侵犯那題後很容易解決。

\(f_{i,j,k}\) 表示 \(1-i\) 行,第 \(i\) 行狀態為 \(j\),第 \(i-1\) 行狀態為 \(k\) 的方案數。

那麼轉移方程如下:

\[f_{i,j,k}=\max\{f_{i-1,k,l}+Sum_j\} \]

保證 \(j,k,l\) 狀態合法。

判斷狀態合法?\((j \& k) || (k \& l) || (l \& j)\)

於是就結束了……

等一等!我 MLE 了!

這也就是在上一篇博文中作者特別提及過的問題。

這道題需要壓縮空間。

兩種方法:

  1. 採用滾動陣列的方式,減少第一維的空間。
    因為這道題的轉移當前行只和上一行有關係,因此第一位只需要開 2,然後利用 \(i \mod 2\) 來轉移即可。
  2. 上篇博文中作者提到過,這道題如果我們將合法狀態輸出,會發現 最多隻有 60 個。 所以完全可以直接壓縮後兩位狀態到 60。

當然可以兩個一起,但是感覺沒什麼用處。

程式碼:

/*
========= Plozia =========
	Author:Plozia
	Problem:P2704 [NOI2001] 炮兵陣地
	Date:2021/3/2
========= Plozia =========
*/

#include <bits/stdc++.h>
#define Max(a, b) ((a > b) ? a : b)

typedef long long LL;
const int MAXN = (1 << 10) + 10, MAXP = 60 + 10;
int n, m, cnt, State[MAXN], Sum[MAXN], a[100 + 10][100 + 10], Map[100 + 10], f[100 + 10][MAXP][MAXP], ans = 0;

int read()
{
	int sum = 0, fh = 1; char ch = getchar();
	for (; ch < '0' || ch > '9'; ch = getchar()) fh -= (ch == '-') << 1;
	for (; ch >= '0' && ch <= '9'; ch = getchar()) sum = (sum << 3) + (sum << 1) + (ch ^ 48);
	return sum * fh;
}

void dfs(int pos, int sum, int num)
{
	if (pos >= m) {State[++cnt] = sum; Sum[cnt] = num; return ;}
	dfs(pos + 1, sum, num);
	dfs(pos + 3, sum + (1 << pos), num + 1);
}

int main()
{
	n = read(), m = read();
	Map[0] = (1 << m) - 1;
	for (int i = 1; i <= n; ++i)
		for (int j = 1; j <= m; ++j)
		{
			char ch; std::cin >> ch;
			if (ch == 'P') a[i][j] = 1;
			Map[i] += a[i][j] * (1 << (m - j));
		}
	dfs(0, 0, 0);
	for (int i = 1; i <= cnt; ++i)
		for (int j = 1; j <= cnt; ++j)
			f[1][i][j] = Sum[i];
	for (int i = 2; i <= n; ++i)
	{
		for (int j = 1; j <= cnt; ++j)
		{
			if (!((Map[i] & State[j]) == State[j])) continue;
			for (int k = 1; k <= cnt; ++k)
			{
				if (!((Map[i - 1] & State[k]) == State[k])) continue;
				for (int l = 1; l <= cnt; ++l)
				{
					if (!((Map[i - 2] & State[l]) == State[l])) continue;
					if ((State[j] & State[k]) || (State[k] & State[l]) || (State[j] & State[l])) continue;
					f[i][j][k] = Max(f[i][j][k], f[i - 1][k][l] + Sum[j]);
				}
			}
		}
	}
	for (int i = 1; i <= cnt; ++i)
		for (int j = 1; j <= cnt; ++j)
			ans = Max(ans, f[n][i][j]);
	printf("%lld\n", ans);
	return 0;
}

P2157 [SDOI2009]學校食堂

先補充一個式子:

\[a \text{ or } b \text{ - } a \text{ and } b = a \text{ xor } b \]

不過不知道這個結論好像也可以做

神仙狀壓 DP 題。

第一眼看上去的時候我傻了:\(1\leq n \leq 1000\).

這怎麼狀壓啊?沒法狀壓啊?

然後我又看了一眼資料:\(0 \leq B_i \leq 7\)

哦那沒事了。

於是我們首先有了一個狀態的雛形:\(f_{i,j}\) 表示當前做到第 \(i\) 個人而且 \([1,i-1]\) 的人全部都拿過了飯的最小等待時間,其中當前第 \(i\) 個人以及其後面 7 個人拿飯組成的狀態為 \(j\)

於是你會發現狀態轉移方程寫不出來。

寫不出來嗎?我們試著寫一寫:

  1. 如果第 \(i\) 個人拿了飯,也就是 \(j \& 1\) 為真,那麼此時 \(i\) 就可以走人,直接轉移時間到 \(f_{i+1,j>>1}\)
  2. 如果第 \(i\) 個人不拿飯,那麼我們需要從後面挑一個人出來拿飯,於是就。。。。。。

你會發現,如果我們不知道上一個拿飯的人是誰,是無法算出轉移新增的時間的!

於是我們引入第三維變數 \(k\) 來記錄上一個拿飯的人,\(k \in [-8,7]\) 表示距離 \(i\) 的位置,也就是上一個拿飯的人是 \(i+k\)

那麼再次寫轉移方程:

  1. 如果第 \(i\) 個人拿了飯,也就是 \(j \& 1\) 為真,那麼此時 \(i\) 就可以走人,直接轉移時間到 \(f_{i+1,j>>1,k-1}\)
  2. 如果第 \(i\) 個人不拿飯,也就是 \(j \& 1\) 為假,此時我們需要從後面挑一個人出來拿飯,假設這個人是 \(i+l\),那麼他將影響到的是 \(f_{i,j|(1<<l),l}\)
    什麼意思呢?由於第 \(i\) 個人不拿飯,那麼沒法轉移到第 \(i+1\),但是第 \(l\) 個人先拿了飯,此時的狀態就會變成 \(j|(1<<l)\),上一個人編號為 \(i+l\)
    但是需要注意的是,考慮到 \(i\) 後面的人可能會有更小的容忍值,那麼此時我們需要變數 \(r\) 來記錄當前最多能夠使誰拿飯(也就是編號最大的),如果超出這個值就說明有人不能容忍了,要立刻停止轉移。

初值:\(f_{1,0,0}=0\),其餘為正無窮。答案:\(\min\{f_{n+1,0,i}|i \in [-8,0]\}\)

需要注意的是,考慮到陣列維度不能開負數,\(k\) 都要加 8,而這就導致了很多細節性的問題,需要注意。

程式碼:

#include <bits/stdc++.h>
#define Min(a, b) ((a < b) ? a : b)
using namespace std;

typedef long long LL;
const int MAXN = 1000 + 10, MAXP = (1 << 8) - 1;
int n, t[MAXN], b[MAXN], f[MAXN][MAXP][20];

int read()
{
	int sum = 0, fh = 1; char ch = getchar();
	for (; ch < '0' || ch > '9'; ch = getchar()) fh -= (ch == '-') << 1;
	for (; ch >= '0' && ch <= '9'; ch = getchar()) sum = (sum << 3) + (sum << 1) + (ch ^ 48);
	return (fh == 1) ? sum : -sum;
}

namespace Plozia
{
	void main()
	{
		n = read();
		memset(t, 0, sizeof(t)); memset(b, 0, sizeof(b));
		for (int i = 1; i <= n; ++i) t[i] = read(), b[i] = read();
		memset(f, 0x3f, sizeof(f));
		f[1][0][7] = 0;
		for (int i = 1; i <= n; ++i)
			for (int j = 0; j <= MAXP; ++j)
				for (int k = -8; k <= 7; ++k)
				{
					if (f[i][j][k + 8] != 0x3f3f3f3f)
					{
						if (j & 1) f[i + 1][j >> 1][k - 1 + 8] = Min(f[i + 1][j >> 1][k - 1 + 8], f[i][j][k + 8]);
						else
						{
							int r = 0x3f3f3f3f;
							for (int l = 0; l <= 7; ++l)
							{
								if ((j >> l) & 1) continue;
								if (i + l > r) break;
								r = Min(r, i + l + b[i + l]);
								f[i][j | (1 << l)][l + 8] = Min(f[i][j | (1 << l)][l + 8], f[i][j][k + 8] + ((i + k) ? (t[i + k] ^ t[i + l]) : 0));
							}
						}
					}
				}
		int ans = 0x3f3f3f3f;
		for (int i = 0; i <= 8; ++i)
			ans = Min(ans, f[n + 1][0][i]);
		printf("%d\n", ans);
		return ;
	}
}

int main()
{
	int t = read();
	while (t--) Plozia::main();
	return 0;
}

P5005 中國象棋 - 擺上馬

相信自己的做法,大喊一聲 :I won't MLE!你就會過這道題。

於是我就 MLE 了。

先假設空間限制為 256 MB,然後來想這道題。

這是一道二維的狀壓 DP,模仿第一題不難想到設 \(f_{i,j,k}\) 表示當前做到第 \(i\) 行,當前行狀態為 \(j\),上一行狀態為 \(k\) 的方案數。

因為馬攻擊範圍可以到上下兩行,所以需要列舉上上行狀態 \(l\)

那麼轉移方程如下:

\[f_{i,j,k}=\sum f_{i-1,k,l} \]

其中保證 \(j,k,l\) 不會互相沖突。

初值:\(f_{1,i,0}=1\),第二行需要特別處理,因為沒有上上行。

於是我們可以先寫下面這樣的程式碼:

for (int i = 0; i < (1 << m); ++i)
		for (int j = 0; j < (1 << m); ++j)
			if (i 與 j 不衝突) f[2][j][i] = (f[2][j][i] + f[1][i][0]) % P;
for (int i = 3; i <= n; ++i)
	for (int j = 0; j < (1 << m); ++j)
		for (int k = 0; k < (1 << m); ++k)
		{
			f[i][j][k] = 0;
			if (j 與 k 衝突) continue;
			for (int l = 0; l < (1 << m); ++l)
			{
				if (k 與 l 衝突) continue;
				if (j 與 l 衝突) continue;
				f[i][j][k] = (f[i][j][k] + f[i - 1][k][l]) % P;
			}
		}
ans = 0;
for (int i = 0; i < (1 << m); ++i)
	for (int j = 0; j < (1 << m); ++j)
		if (i 與 j 不衝突) ans = (ans + f[n][i][j]) % P;

然後來考慮怎麼處理衝突問題。

衝突分兩種:兩行衝突(Two_attack)和三行衝突(Three_attack)。

  • 兩行衝突:也就是單行對下一行的攻擊是否與下一行衝突。
  • 三行衝突:也就是第一行的跨行攻擊是否對第三行衝突。

不好理解?那就對了,反正我說的也不是人話, 上圖!

兩行衝突:

從右往左考慮。記當前狀態為 10110110

第一個格子沒有馬,跳過。

第二個格子有馬,那麼這個格子右邊有馬嗎?沒有。於是可以向右攻擊。但是他左邊有馬,於是不能想左攻擊。

那麼就變成了這樣:

第三個格子,右邊有馬,左邊沒有馬,那麼可以向左邊攻擊。

這麼迴圈反覆,最後就變成了這樣:

三行衝突(暫且不考慮兩行衝突):

還是考慮第一行,發現右數第二格有馬,而且沒被擋住,那麼可以向下面兩行攻擊。

然後右數第三個,有馬且沒被擋住,可以向下攻擊。

那麼繼續做下去,發現只有第一列的馬被擋住,那麼最後結果如下:

於是就做完了。

關於程式碼實現:

首先我們需要兩個基礎函式:

int Getbit(int x, int a)//返回 x 的二進位制下第 a 位且保留右側 0
{
	if (a < 1) return 0;
	return x & (1 << (a - 1));
}

int check(int x, int a)//查詢 x 的二進位制下第 a 位
{
	if (a < 1) return 0;
	if (x & (1 << (a - 1))) return 1;
	return 0;
}

然後就可以愉快的打程式碼了。

這裡有一個小技巧:-1 的補碼是 11111111111111111111111111111111(32 個1),可以利用這個來處理位運算。

Two_attackThree_attack 如下:

int Two_attack(int k)//k 是上面一行狀態
{
	int State = 0;
	for (int i = 1; Getbit(-1, i) <= k; ++i)
	{
		if (!check(k, i)) continue;
		if (!check(k, i - 1)) State |= Getbit(-1, i - 2);
		if (!check(k, i + 1)) State |= Getbit(-1, i + 2);
	}
	return State;
}

int Three_attack(int k, int l)//k 是第一行,l 是第二行
{
	int State = 0;
	for (int i = 1; Getbit(-1, i) <= k; ++i)
	{
		if (!check(k, i)) continue;
		if (!check(l, i)) State |= Getbit(-1, i - 1), State |= Getbit(-1, i + 1);
	}
	return State;
}

那麼就做完了。

特別提醒:因為本題毒瘤的空間限制,必須使用滾動陣列壓縮空間。

程式碼:

/*
========= Plozia =========
	Author:Plozia
	Problem:P5005 中國象棋 - 擺上馬
	Date:2021/3/5
========= Plozia =========
*/

#include <bits/stdc++.h>

typedef long long LL;
const int P = 1e9 + 7;
int n, m;
LL f[3][64][64], ans;

int read()
{
	int sum = 0, fh = 1; char ch = getchar();
	for (; ch < '0' || ch > '9'; ch = getchar()) fh -= (ch == '-') << 1;
	for (; ch >= '0' && ch <= '9'; ch = getchar()) sum = (sum << 3) + (sum << 1) + (ch ^ 48);
	return (fh == 1) ? sum : -sum;
}

int Getbit(int x, int a)//返回 x 的二進位制下第 a 位且保留右側 0
{
	if (a < 1) return 0;
	return x & (1 << (a - 1));
}

int check(int x, int a)//查詢 x 的二進位制下第 a 位
{
	if (a < 1) return 0;
	if (x & (1 << (a - 1))) return 1;
	return 0;
}

int Two_attack(int k)//k 是上面一行狀態
{
	int State = 0;
	for (int i = 1; Getbit(-1, i) <= k; ++i)
	{
		if (!check(k, i)) continue;
		if (!check(k, i - 1)) State |= Getbit(-1, i - 2);
		if (!check(k, i + 1)) State |= Getbit(-1, i + 2);
	}
	return State;
}

int Three_attack(int k, int l)//k 是第一行,l 是第二行
{
	int State = 0;
	for (int i = 1; Getbit(-1, i) <= k; ++i)
	{
		if (!check(k, i)) continue;
		if (!check(l, i)) State |= Getbit(-1, i - 1), State |= Getbit(-1, i + 1);
	}
	return State;
}

int main()
{
	n = read(), m = read();
	for (int i = 0; i < (1 << m); ++i) f[1][i][0] = 1;
	for (int i = 0; i < (1 << m); ++i)
		for (int j = 0; j < (1 << m); ++j)
			if ((!(Two_attack(i) & j)) & (!(Two_attack(j) & i))) f[2][j][i] = (f[2][j][i] + f[1][i][0]) % P;
	for (int i = 3; i <= n; ++i)
		for (int j = 0; j < (1 << m); ++j)
			for (int k = 0; k < (1 << m); ++k)
			{
				f[i % 3][j][k] = 0;
				if (Two_attack(j) & k) continue;
				if (Two_attack(k) & j) continue;
				for (int l = 0; l < (1 << m); ++l)
				{
					if (Two_attack(l) & k) continue;
					if (Two_attack(k) & l) continue;
					if (Three_attack(l, k) & j) continue;
					if (Three_attack(j, k) & l) continue;
					f[i % 3][j][k] = (f[i % 3][j][k] + f[(i - 1) % 3][k][l]) % P;
				}
			}
	ans = 0;
	for (int i = 0; i < (1 << m); ++i)
		for (int j = 0; j < (1 << m); ++j)
			if ((!(Two_attack(i) & j)) & (!(Two_attack(j) & i))) ans = (ans + f[n % 3][i][j]) % P;
	printf("%lld\n", ans);
	return 0;
}

3. 總結

狀壓 DP 還是非常靈活的,非常考驗思維能力以及程式碼能力,需要多加練習。