1. 程式人生 > 實用技巧 >2019CSUST個人選拔-我愛吃燒烤(狀壓DP)

2019CSUST個人選拔-我愛吃燒烤(狀壓DP)

題目連結:http://acm.csust.edu.cn/problem/2007
CSDN食用連結:https://blog.csdn.net/qq_43906000/article/details/107654460
Description

燒烤真的很好吃唉!集訓隊的團建除了佰燒,下館子就是燒烤啦!

這天集訓隊一群毒瘤想出去吃燒烤,這裡一共有\(n\)個燒烤店,編號\(1,2,...,n\),這\(n\)個燒烤店中有\(m\)個特殊的燒烤店,初始時大家在1號燒烤店,他們想嘗試其中至少\(k\)個不同的特殊的燒烤店。從任意兩個燒烤店\(x,y\)走過去消耗的體力值都為\(1\),注意你在當前的燒烤店停留一次也會消耗\(1\)點體力值。\(mp[i][j]\)

表示從燒烤店\(i\)\(j\)的方案數。問他們恰好消耗\(Q\)點體力值且能品嚐到至少\(k\)個不同的特殊燒烤店的方案數。資料保證11號燒烤店不是特殊的燒烤店。答案可能很大,輸出模\(20190802\)後的值。

Input
第一行五個整數,分別表示\(n,m,k,Q\)

接下來一行\(m\)個整數\(a_i\)表示特殊燒烤店的編號。

接下來一個\(n\)\(n\)列的矩陣\(mp\),意義如題。

\(1\leq n,Q\leq 50,0\leq m,k\leq 10,0\leq mp[i][j]\leq1000\)

Output
輸出一行一個整數表示答案。

Sample Input 1
5 1 1 4
5
2 1 0 0 0
3 1 1 0 0
4 1 0 1 0
5 1 0 0 1
6 1 0 0 0

Sample Output 1
1

Sample Input 2
1 0 0 10
26

Sample Output 2
14277670

Sample Input 3
11 2 2 10
6 11
1 1 0 0 0 0 1 0 0 0 0
2 1 1 0 0 0 1 0 0 0 0
3 1 0 1 0 0 1 0 0 0 0
4 1 0 0 1 0 1 0 0 0 0
5 1 0 0 0 1 1 0 0 0 0
6 1 0 0 0 0 1 0 0 0 0
7 1 0 0 0 0 1 1 0 0 0
8 1 0 0 0 0 1 0 1 0 0
9 1 0 0 0 0 1 0 0 1 0
10 1 0 0 0 0 1 0 0 0 1
11 1 0 0 0 0 1 0 0 0 0
Sample Output 3
2

Hint

對於樣例1:你只有一種走法能在規定體力消耗內吃到至少一個特殊燒烤店:1->2->3->4->5,方案數為\(1 * 1 * 1 * 1=1\)

對於樣例2:要求吃到至少0個特殊燒烤店,也就是說你可以一個特殊燒烤店都不去,方案數為26^10 mod 20190802 =14277670

emmm,這道題挺好想的QAQ,至少現在看來是這樣的。

看題目的資料範圍,我們很容易知道用狀壓DP來解決,我們狀壓m個特殊的店於是就有了\(dp[1<<11]\),考慮到要恰好消耗\(Q\)點體力,所以我們要再加上一維,就變成了了\(dp[1<<11][55]\),但還有問題沒有解決,也就是最後停留的點可能是\(1-n\)中的任意一點,所以我們還要再加上一維停留點:\(dp[1<<11][55][55]\)。計算一下空間,發現差不多了,應該不用再加了

接下來就是狀態轉移了,其中上面所說的三維肯定要列舉的,我們直接列舉上一個狀態,然後再列舉上一個狀態的最終停留點,再列舉上一個點所消耗的體力,那麼要做狀態轉移的話肯定還要加上現在要去的點,於是就有了以下程式碼段:

dp[0][0][1]=1;
for (int i=0; i<(1<<m); i++) {
	for (int last=1; last<=n; last++) {
		if (vis[last] && !judge(i,1<<(vis[last]-1))) continue;
		//vis記錄的是特殊點的編號,如果上一個點是特殊點,那麼一定不會和上一個特殊點狀態集矛盾
		for (int pw=0; pw<q; pw++) {
			if (!dp[i][pw][last]) continue;//小優化
			for (int now=1; now<=n; now++) {
				if (!mp[last][now]) continue;//小優化
				/*DP*/
			}
		}
	}
}

然後計算一波時間複雜度。。。\(O(2000*50*50*50)\),emmm,讓我冷靜一波,感覺似乎不能優化了啊,想法也應該沒什麼毛病,沒辦法了,只能硬著頭皮剛一波再說了,說不定資料跑不滿(水)呢,於是就有了以上的小優化。

接下來我們考慮轉移,對於轉移,應該有兩種方式,第一個是現在要去的點為特殊點的時候,另一個就是非特殊點的時候,那麼就有了一下轉移方程:

if (vis[now]) {
	int sta=i|(1<<(vis[now]-1));
	dp[sta][pw+1][now]=(dp[sta][pw+1][now]+dp[i][pw][last]*mp[last][now])%mod;
} 
else dp[i][pw+1][now]=(dp[i][pw+1][now]+dp[i][pw][last]*mp[last][now])%mod;

最後列舉一下最終狀態和落腳點就行了。。。然後你就會發現。。你似乎過不了樣例????

感覺天衣無縫啊,冷靜分析一波。似乎體力的列舉不應該在裡面,每次列舉消耗一個體力的時候應該跑完整個圖的,那麼也就是說體力的列舉應該放在最外面,然後\(try\)一波。。。。AC!媽媽,我終於會DP了QAQ

以下是AC程式碼:

#include <bits/stdc++.h>
using namespace std;

typedef long long ll;
const int mod=20190802;

ll dp[1<<11][55][55];//狀態為sta,消耗q點體力,當前點為x號點的方案數
int b[100],mp[55][55],ok[1<<11];
int vis[55];

int digt(int x)
{
	int ans=0;
	while (x){
		if (x&1) ans++;
		x>>=1; 
	}
	return ans;
}

void pre_oksta(int m,int k)
{
	for (int i=0; i<(1<<m); i++){
		int nb=digt(i);
		if (nb>=k) ok[i]=1;
	}
}

int judge(int x,int y)
{
	for (int i=0; i<=10; i++)
		if (!(x&(1<<i)) && (y&(1<<i))) return 0;
	return 1;
}

int main(int argc, char const *argv[])
{
	int n,m,k,q;
	scanf ("%d%d%d%d",&n,&m,&k,&q);
	for (int i=1; i<=m; i++) scanf ("%d",&b[i]),vis[b[i]]=i;
	for (int i=1; i<=n; i++)
		for (int j=1; j<=n; j++)
			scanf ("%d",&mp[i][j]);
	pre_oksta(m,k);
	dp[0][0][1]=1;
	for (int pw=0; pw<q; pw++){//列舉體力
		for (int i=0; i<(1<<m); i++){//列舉上一個狀態
			for (int last=1; last<=n; last++){//列舉上一個落腳點
				if (vis[last] && !judge(i,1<<(vis[last]-1))) continue;
				if (!dp[i][pw][last]) continue;
				for (int now=1; now<=n; now++){//列舉現在要去的
					if (!mp[last][now]) continue;
					if (vis[now]){
						int sta=i|(1<<(vis[now]-1));
						dp[sta][pw+1][now]=(dp[sta][pw+1][now]+dp[i][pw][last]*mp[last][now])%mod;
					} 
					else dp[i][pw+1][now]=(dp[i][pw+1][now]+dp[i][pw][last]*mp[last][now])%mod;
				}
			}
		}
	}
	ll ans=0;
	for (int i=0; i<(1<<(m+1)); i++){
		if (!ok[i]) continue;
		for (int j=1; j<=n; j++)
			ans=(ans+dp[i][q][j])%mod;
	}
	printf("%lld\n",ans);
	return 0;
}