CSUST 2007-我爱吃烧烤(状压DP)

题目链接:http://acm.csust.edu.cn/problem/2007
博客园食用链接:https://www.cnblogs.com/lonely-wind-/p/13395416.html
Description

烧烤真的很好吃唉!集训队的团建除了佰烧,下馆子就是烧烤啦!

这天集训队一群毒瘤想出去吃烧烤,这里一共有 n n n个烧烤店,编号 1 , 2 , . . . , n 1,2,...,n 1,2,...,n,这 n n n个烧烤店中有 m m m个特殊的烧烤店,初始时大家在1号烧烤店,他们想尝试其中至少 k k k个不同的特殊的烧烤店。从任意两个烧烤店 x , y x,y x,y走过去消耗的体力值都为 1 1 1,注意你在当前的烧烤店停留一次也会消耗 1 1 1点体力值。 m p [ i ] [ j ] mp[i][j] mp[i][j]表示从烧烤店 i i i j j j的方案数。问他们恰好消耗 Q Q Q点体力值且能品尝到至少 k k k个不同的特殊烧烤店的方案数。数据保证11号烧烤店不是特殊的烧烤店。答案可能很大,输出模 20190802 20190802 20190802后的值。

Input
第一行五个整数,分别表示 n , m , k , Q n,m,k,Q n,m,k,Q

接下来一行 m m m个整数 a i a_i ai表示特殊烧烤店的编号。

接下来一个 n n n n n n列的矩阵 m p mp mp,意义如题。

1 ≤ n , Q ≤ 50 , 0 ≤ m , k ≤ 10 , 0 ≤ m p [ i ] [ j ] ≤ 1000 1\leq n,Q\leq 50,0\leq m,k\leq 10,0\leq mp[i][j]\leq1000 1n,Q50,0m,k10,0mp[i][j]1000

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 1 * 1 * 1 * 1=1 1111=1

对于样例2:要求吃到至少0个特殊烧烤店,也就是说你可以一个特殊烧烤店都不去,方案数为26^10 mod 20190802 =14277670

emmm,这道题挺好想的QAQ,至少现在看来是这样的。

看题目的数据范围,我们很容易知道用状压DP来解决,我们状压m个特殊的店于是就有了 d p [ 1 < < 11 ] dp[1<<11] dp[1<<11],考虑到要恰好消耗 Q Q Q点体力,所以我们要再加上一维,就变成了了 d p [ 1 < < 11 ] [ 55 ] dp[1<<11][55] dp[1<<11][55],但还有问题没有解决,也就是最后停留的点可能是 1 − n 1-n 1n中的任意一点,所以我们还要再加上一维停留点: d p [ 1 < < 11 ] [ 55 ] [ 55 ] dp[1<<11][55][55] 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 ) O(2000*50*50*50) O(2000505050),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;

最后枚举一下最终状态和落脚点就行了。。。然后你就会发现。。你似乎过不了样例????

感觉天衣无缝啊,冷静分析一波。似乎体力的枚举不应该在里面,每次枚举消耗一个体力的时候应该跑完整个图的,那么也就是说体力的枚举应该放在最外面,然后 t r y try 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;
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值