(2021-3-19编写)【CSP202012-04】食材运输-状压DP+树形DP

测试地址:食材运输

题目大意: n ( n ≤ 100 ) n(n\le 100) n(n100)个节点的树,有边权。每个点有各自的需求,一共可能有 K ( K ≤ 10 ) K(K\le 10) K(K10)种需求,每个点也可以有多种需求。对于每种需求,需要派遣一辆(且只能派遣一辆)车,从某点开始,顺着树边遍历所有有这种需求的点,从而满足这些点的需求,边权就是通过这条边所需的时间。现在要求安排 M ( M ≤ K ) M(M\le K) M(MK)个点,使得当所有 K K K辆车同时从这 M M M个点中的某些点出发后,等待需求被满足的时间最长的点等待时间最短。求这个最优等待时间。

做法: 本题需要用到状压DP+树形DP。

…上了大学后我本来不想再搞竞赛了,ACM也没打,许许多多的知识也全都抛之脑后了。不过为了争取免修程设课,还是选择了CSP这个…东西,结果没想到这个比赛不都是简单题,而是简单题、毒瘤题和有意思题的结合。就2020年12月这套而言,我感觉只有这道题比较有意思。

题意比较的绕,我一开始读也没读懂。耐心理解后发现这是一个一环套一环的多层决策问题:不但要选择出发点,而且要选择哪些车从哪个出发点出发,还要选择每辆车所走的路径。

既然这是一个多层决策问题,那么一般从最内层着手是最有效的。那么我们先考虑固定一种需求(简单来说就是固定一个需要遍历的点集),固定一个出发点,如何通过选择路径不同,使所有点等待时间的最大值取到最小值

很显然,只有最后到达的点的等待时间是可能会对所有点等待时间最大值有贡献的。所以我们只用考虑路径的长度如何取到最小即可。

这里有一个结论:从某一点出发,在树上遍历一个点集的最短路径长度,等于所有将出发点和所要遍历的点集连起来的边(也就是指,将这些点两两之间的路径涂黑后,被涂黑的所有的边)的长度和 × 2 \times 2 ×2,再减去出发点到要遍历的点的最长路径。

直观理解的话,就是从出发点开始,像DFS遍历树一样遍历所有要遍历的点,只不过选择最后遍历离出发点最远的那个点,那么除了出发点和这个点之间的路径上的边只被经过一次外,其它在这些点两两之间路径上的点都被经过了两次(来回)。可以证明这是最优策略。

那么,若固定一种需求和一个出发点,用类似树形DP的方法求出最优值的时间复杂度为 O ( n ) O(n) O(n)。这时我们可以试一试解决更外层的决策。由于最外层的决策是选取 M M M个出发点,因此我们应该先考虑:固定一个出发点,要求只用这一个出发点满足所有需求的一个子集 S S S,所有点等待时间的最大值的最小值。

对于一个需求和一个出发点,最优路径是一条,那么对于多个需求和一个出发点,多条最优路径中最长的,就是这个外层决策的最优值。我们用 g ( v , S ) g(v,S) g(v,S)来表示出发点为 v v v,需求集合为 S S S时的最优值。

这时就可以考虑最外层的决策:选择出发点了。通过观察数据范围,而且发现在选择出发点时,还得跟着选择分配给该出发点的需求,可以想到使用状压DP来解决这个问题。令 d p ( i , S ) dp(i,S) dp(i,S)为已经选取了 i i i个出发点,且已经覆盖到了 S S S这个集合中的所有需求时(也就是说 S S S为所有需求的一个子集),所有点等待时间最大值的最小值。那么有状态转移方程:

d p ( i , S ) = min ⁡ { max ⁡ { d p ( 1 , T ) , d p ( i − 1 , S − T ) } } dp(i,S)=\min\{\max\{dp(1,T),dp(i-1,S-T)\}\} dp(i,S)=min{max{dp(1,T),dp(i1,ST)}}
其中最外层的 min ⁡ \min min是在枚举 T T T S S S的所有子集。

这时我们只要对任意 S S S d p ( 1 , S ) dp(1,S) dp(1,S)即可解决这个问题,易得 d p ( 1 , S ) dp(1,S) dp(1,S)和之前的 g ( v , S ) g(v,S) g(v,S)之间的关系:

d p ( 1 , S ) = min ⁡ { g ( v , S ) } dp(1,S)=\min\{g(v,S)\} dp(1,S)=min{g(v,S)}
min ⁡ \min min枚举 v v v:所有的出发点。

这一串决策里取 min ⁡ \min min还是取 max ⁡ \max max容易搞混,这就要求选手思路一定要清晰。

于是经历了这么长的讨论,我们按着从内到外的顺序理一遍本题的解题思路,外加计算一下时间复杂度:

首先,对于所有出发点 v v v和需求集合 S S S,求出 g ( v , S ) g(v,S) g(v,S)。其中先对每个单个需求,用树形DP的方法求最优路径,时间复杂度为 O ( n 2 K ) O(n2^K) O(n2K),然后对每个 S S S,枚举其中所有的单个需求,对最优路径求最大值,时间复杂度为 O ( 2 K ) O(2^K) O(2K)

然后,对所有需求集合 S S S,求出 d p ( 1 , S ) dp(1,S) dp(1,S),因为对每个 S S S求时要枚举 v v v,所以时间复杂度为 O ( n 2 K ) O(n2^K) O(n2K)

最后,状压DP求出最后的答案: d p ( M , S ) dp(M,S) dp(M,S) S S S为所有需求),由于求时要枚举子集,时间复杂度为 O ( M 3 K ) O(M3^K) O(M3K)

每一步的时间复杂度都可以接受,所以我们最终解决了这一题。

以下是本人代码:

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

int n,m,k;
int need[110]={0},g[110][(1<<10)+10],dp[(1<<10)+10][11];
vector<int> edge[110],w[110];
bool p[110]={0};

int calc_sum(int v, int fa)
{
	int ans=-1;
	int tmp;
	
	if (p[v]) ans=0;
	
	for(int i=0;i<edge[v].size();i++)
		if (edge[v][i]!=fa)
		{
			tmp=calc_sum(edge[v][i],v);
			if (tmp!=-1)
			{
				if (ans==-1) ans=0;
				ans+=w[v][i]+tmp;
			}
		}
	
	return ans;
}

int find_furthest(int v, int fa)
{
	int ans=-1;
	int tmp;
	
	if (p[v]) ans=0;
	
	for(int i=0;i<edge[v].size();i++)
		if (edge[v][i]!=fa)
		{
			tmp=find_furthest(edge[v][i],v);
			if (tmp!=-1&&tmp+w[v][i]>ans)
				ans=tmp+w[v][i];
		}
	
	return ans;
}

void calc(int now)
{
	memset(p,0,sizeof(p));
	
	int ans,sum;
	
	for(int i=1;i<=n;i++)
		if (need[i]&(1<<now)) p[i]=1;
	
	for(int i=1;i<=n;i++)
	{
		bool flag=p[i];
		p[i]=1;
		sum=calc_sum(i,0);
		g[i][1<<now]=(sum<<1)-find_furthest(i,0);
		p[i]=flag;
	}
	
	for(int i=0;i<(1<<now);i++)
	{
		dp[i+(1<<now)][1]=2000000000;
		for(int j=1;j<=n;j++)
		{
			g[j][i+(1<<now)]=max(g[j][i],g[j][1<<now]);
			dp[i+(1<<now)][1]=min(dp[i+(1<<now)][1],g[j][i+(1<<now)]);
		}
	}
}

int main()
{
	cin >> n >> m >> k;
	
	for(int i=1;i<=n;i++)
		for(int j=0;j<k;j++)
		{
			int tmp;
			cin >> tmp;
			need[i]+=tmp*(1<<j);
		}
	
	for(int i=1;i<n;i++)
	{
		int a,b,W;
		cin >> a >> b >> W;
		edge[a].push_back(b);
		w[a].push_back(W);
		edge[b].push_back(a);
		w[b].push_back(W);
	}
	
	dp[0][1]=0;
	for(int i=0;i<k;i++)
		calc(i);
	
	for(int i=0;i<(1<<k);i++)
		for(int j=2;j<=m;j++)
		{
			dp[i][j]=dp[i][1];
			for(int s=i;s;s=((s-1)&i))
				dp[i][j]=min(dp[i][j],max(dp[i-s][1],dp[s][j-1]));
		}
	
	cout << dp[(1<<k)-1][m];
	
	return 0;
}

  • 6
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值