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]\)
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;
}