Luogu P5005 中國象棋 - 擺上馬 / Luogu P8756 國際象棋 題解 [ 藍 ] [ 狀壓 dp ] [ 位運算 ]

KS_Fszha發表於2024-08-02

國際象棋:模板棋盤狀壓。
擺上馬:需要點思維的棋盤狀壓,相比上一道題加了“蹩馬腳”的設定。

Easy_version :國際象棋

概述一下此類棋盤問題的思路:

  1. 用二進位制數表示出棋盤上某一行的狀態。
  2. 用位運算預處理出合法的單行狀態,以及需要用到的一些東西。
  3. 用位運算判斷前一行或者前幾行能否轉移過來。
  4. 轉移前一行或者前幾行的相容類。

基礎位運算

判斷兩邊是否有:i&(i>>1)i&(i<<1)
判斷 \(i\) 是否包含在合法狀態 \(j\) 裡: (i&j)==i

對於本題

記錄下兩行的狀態,定義 \(dp[i][j][k][l]\) 表示第 \(i\) 行時,第 \(i\)行狀態為 \(j\) ,第 \(i-1\) 行狀態為 \(k\) ,且目前擺了 \(l\) 個馬的方案數。

初始化 \(dp[0][0][0][0]=1\)

迴圈順序是:

  1. 行數。
  2. 馬的個數。
  3. \(i\) 行狀態。
  4. \(i-1\) 行狀態。
  5. \(i-2\) 行狀態。

注意由於只會用到前一行的狀態,所以我們可以強行滾動陣列最佳化,透過 \(\bmod 2\) 來解決。滾動陣列每次必須先初始化為 \(0\)

時間複雜度 \(O(2^{3n}mk)\),有點緊,常數不能太大。

為了不在最後統計一遍,太過麻煩,所以我們多加了兩行,並強制這兩行必須不能放馬,這樣就不用手工統計方案數了。

程式碼

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int,int> pi;
int n,m,k,tot[70];
const ll mod=1e9+7;
ll dp[2][70][70][25];
int main()
{
	ios::sync_with_stdio(0);
	cin.tie(0);
	cout.tie(0);
	cin>>n>>m>>k;
	for(int i=0;i<(1<<n);i++)
	{
		for(int j=0;j<n;j++)
		{
			tot[i]+=((i>>j)&1);
		}
	}
	dp[0][0][0][0]=1;
	for(int i=1;i<=m+2;i++)
	{
		for(int j=0;j<=k;j++)
		{
			for(int a=0;a<(1<<n);a++)
			{
				for(int b=0;b<(1<<n);b++)
				{
					dp[i&1][a][b][j]=0;
					if(((a>>2)&b)||((a<<2)&b))continue;
					for(int c=0;c<(1<<n);c++)
					{
						if(((a>>1)&c)||((a<<1)&c))continue;
						if(tot[a]<=j)
						{
							dp[i&1][a][b][j]=(dp[i&1][a][b][j]+dp[(i&1)^1][b][c][j-tot[a]])%mod;
						}
					}
				}
			}
		}
	}
	cout<<dp[(m+2)&1][0][0][k];
	return 0;
}

Hard_version :中國象棋 - 擺上馬

相比上一道,這道必須考慮蹩馬腳的情況,且不用記錄馬的個數。

這題難點便是在判斷蹩馬腳:

進階位運算

篩選出本行左邊是 \(0\) ,這一位是 \(1\) 的點: i&((~i)>>1)
篩選出本行右邊是 \(0\) ,這一位是 \(1\) 的點: i&((~i)<<1)

考慮 \(i\)\(i-1\) 行時

\(a\) 為第 \(i\) 行狀態,\(b\) 為第 \(i-1\) 行狀態,\(c\) 為第 \(i-2\) 行狀態。

\(i\) 行左邊沒有蹩馬腳,並且可以攻擊到左上的馬的情況:a&((~a)>>1)&(b>>2)
\(i\) 行右邊沒有蹩馬腳,並且可以攻擊到右上的馬的情況:a&((~a)<<1)&(b<<2)

\(i-1\) 行左邊沒有蹩馬腳,並且可以攻擊到左下的馬的情況:b&((~b)>>1)&(a>>2)
\(i-1\) 行右邊沒有蹩馬腳,並且可以攻擊到右下的馬的情況:b&((~b)<<1)&(a<<2)

考慮 \(i\)\(i-2\) 行時

\(i\) 行上邊沒有蹩馬腳,並且可以攻擊到左上的馬的情況:a&(~b)&(c>>1)
\(i\) 行上邊沒有蹩馬腳,並且可以攻擊到右上的馬的情況:a&(~b)&(c<<1)

\(i-2\) 行下邊沒有蹩馬腳,並且可以攻擊到左下的馬的情況:c&(~b)&(a>>1)
\(i-2\) 行下邊沒有蹩馬腳,並且可以攻擊到右下的馬的情況:c&(~b)&(a<<1)

時間複雜度 \(O(2^{3y}x)\)

程式碼

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int,int> pi;
int n,m;
const ll mod=1e9+7;
ll dp[2][70][70];
int main()
{
	ios::sync_with_stdio(0);
	cin.tie(0);
	cout.tie(0);
	cin>>m>>n;
	dp[0][0][0]=1;
	for(int i=1;i<=m+2;i++)
	{
		for(int a=0;a<(1<<n);a++)
		{
			for(int b=0;b<(1<<n);b++)
			{
				dp[i&1][a][b]=0;
				if((a&((~a)>>1)&(b>>2))||((a&((~a)<<1)&(b<<2)))||(b&((~b)>>1)&(a>>2))||(b&((~b)<<1)&(a<<2)))continue;
				for(int c=0;c<(1<<n);c++)
				{
					if(((~b)&a&(c>>1))||((~b)&a&(c<<1))||((~b)&c&(a<<1))||((~b)&c&(a>>1)))continue;
					dp[i&1][a][b]=(dp[i&1][a][b]+dp[(i&1)^1][b][c])%mod;
				}
			}
		}
	}
	cout<<dp[(m+2)&1][0][0];
	return 0;
}

相關文章