#P2058. 全国动员

#P2058. 全国动员

题目描述

X国有\(n\)个城市,\(n\)个城市之间相互连通,且互相只有一条路可以到达,首都在\(1\)号城市,现在有\(m\)项任务需要完成,每个城市都可以选择来完成一项任务,也可以不选择做。完成的任务需要传输到首都。传输是需要时间的,完成任务也需要时间,请计算完成所有任务最少的时间。

输入格式

第一行是2个整数\(n,m\)

接下来n-1行,每行3个整数\(x,y,w\)表示城市x到y有一条路,传输时间为\(w\)

接下来是\(n\)\(m\)列个整数,第\(i\)\(j\)列表示城市\(i\)完成任务\(j\)需要的时间

输出格式

一个整数,表示答案,范围在int范围

样例

输入数据 1
6 3
1 2 1
1 3 1
2 4 1
2 5 1
3 6 1
3 4 3
3 2 5
6 1 2
1 8 9
8 8 1
4 7 6
输出数据 1
7

数据规模与约定

50%的数据有 n<=10, m<=5。

100%数据n<=55, m<=11,保证 n>=m。

Solution

刚看到这道题以为是一道正常的树形 \(\text{DP}\) ,结果推了半天的方程也没推出来,然后看了看数据范围……好吧肯定有状压,压缩的内容一定与 \(m\) 有关。

\(m\) 的规模入手,那么使用状态压缩的话与 \(m\) 相关,因此状态压缩应该存储的是任务的完成情况,显然这个大小是 \(2^{11}\) 的,完全可以存的下。接下来就可以按照正常的树形 \(\text{DP}\) 来进行思考了。

\(f[x][i]\) 是以 \(x\) 为根的子树中任务完成状态为 \(i\) 的最小时间。考虑 \(f[x][i]\) 的来源,首先如果 \(x\) 是叶子节点,那么 \(f[x][i]\) 一定需要保证__builting_popcount(i)==1\(i\) 二进制上只有一个为 \(1\)),因为叶子结点总共只能完成一个任务,因此对于叶子结点的更新就很简单,直接 \(f[x][(1<<(k-1))]=cost[x][k]\)\(k\) 表示第 \(k\) 个任务)。

然后需要思考的就是对于非叶节点的转移。很明显,非叶节点应该有两个状态来源:

自己不做任务

对于这一种情况, \(f[x][i]\) 就是完全从子节点转移来的。那可以从子节点的哪些状态转移来呢?显然,只能从 \(i\) 的子集转移来,但是又出现了一个非常棘手的问题,就是我并不知道每个子节点到底会对父节点的那些状态贡献多少,如果一个一个枚举的话时间复杂度直接飙升至 \(\text O(n2^{2m})\) ,对于本题来说完全卡不过去。那么该如何更新这种状态呢?

因为我们是一个一个向下 \(\text{DFS}\) 子节点的,所以前面子节点对父节点产生的贡献已经算在 \(f\) 内了,所以我们只需要调用 \(f\) 内存储的信息即可,具体方程就是这样的: \(f[x][i]=\min\{f[x][i],f[son_x][t]+f[x][i\text{^} t]\},t \subseteq i\) 。值得注意的是,因为更新 \(f[x][i]\) 时访问的 \(f\) 数组的第二维下标一定都是小于 \(i\) 的,而且我们要避免访问的 \(f\) 已经被当前结点更新(否则会出现一个节点做了好几个任务的情况),所以对于 \(i\) 的枚举应该从大往小(可以用 \(01\) 背包理解)。

自己做一个任务

这种情况在上一种情况的基础上其实很好转移。假设当前结点 \(x\) 做了一个任务 \(t\) ,变成的状态是 \(i\) ,那么即可将其分解为为是当前节点 \(x\) 在状态 \(i\text{^}(1<<(t-1))\) 没做任务的最小时间,完成任务 \(t\) 所需时间。知道这一点即可推出转移方程: \(f[x][i]=\min\{f[x][i],f[x][i\text ^(1<<(t-1))]+cost[x][t]\}\) 。同样需要注意的是,这里更新 \(f[x][i]\) 时访问的 \(f\) 也都位于小于 \(i\) 的位置,所以也需要枚举 \(i\) 的时候倒序枚举来避免多做任务。

开始前初始化 \(f\) 为一个极大值,但是需要注意这个极大值不能太大,因为在转移的过程中涉及到加法的操作,如果直接无脑 0x7fffffff ,会导致加法计算时溢出,导致答案变成负数,然后喜提 \(\text{WA}\) (当然如果你要用 \(\text{unsigned}\) 我也没办法)。另外需要注意, \(f\) 的所有第二维为 \(0\) 的时候初值都应该赋为 \(0\) (既然啥任务都不做那时间肯定就是 \(0\) 了啊)。

另外,在代码实现的时候,需要注意上面 \(\text{DP}\) 转移的先后顺序,自己做任务的转移应该放在自己不做任务转移的后面,因为自己做任务的转移是需要用到不做任务转移的信息的,具体阐释上面已经写了。

#include<cstdio>
#include<algorithm>
#include<cstring>
#include<limits.h>
#include<cmath>
#define mem(a,b) memset(a,b,sizeof(a))
using namespace std;
template<typename T> void read(T &k)
{
 	k=0;
	T flag=1;char b=getchar();
	while (b<'0' || b>'9') {flag=(b=='-')?-1:1;b=getchar();}
	while (b>='0' && b<='9') {k=(k<<3)+(k<<1)+(b^48);b=getchar();}
	k*=flag;
}
const int _SIZE=55,_STATSIZE=(1<<11);
int n,m;
struct EDGE{
	int next,to,len;
}edge[(_SIZE<<1)+5];
int tot,head[_SIZE+5],InDe[_SIZE+5];
void AddEdge(int x,int y,int v)
{
	++tot;
	edge[tot].next=head[x];
	edge[tot].to=y;
	edge[tot].len=v;
	InDe[y]++;
	head[x]=tot;
}
int f[_SIZE+5][_STATSIZE+5];
int cost[_SIZE+5][_SIZE+5];
void dfs(int x,int fa)
{
	for (int i=head[x];i;i=edge[i].next)
	{
		int twd=edge[i].to,c=edge[i].len;
		if (twd==fa) continue;
		dfs(twd,x);
		for (int i=(1<<m)-1;i;i--)
			for (int t=i;t;t=(t-1)&i)
			{
				f[x][i]=min(f[twd][t]+f[x][i^t]+c,f[x][i]);
			}
	}
	for (int i=(1<<m)-1;i;i--)
		for (int k=0;k<m;k++)
			if (i>>k&1) f[x][i]=min(f[x][i],f[x][i^(1<<k)]+cost[x][k+1]);
}
int main()
{
	read(n),read(m);
	for (int i=1;i<n;i++)
	{
		int a,b,c;
		read(a),read(b),read(c);
		AddEdge(a,b,c);
		AddEdge(b,a,c);
	}
	for (int i=1;i<=n;i++)
		for(int j=1;j<=m;j++)
			read(cost[i][j]);
	for (int i=1;i<=n;i++)
		for (int j=1;j<1<<m;j++)
			f[i][j]=INT_MAX>>2;//防止溢出
	dfs(1,0);
	printf("%d\n",f[1][(1<<m)-1]);
	return 0;
}

又是一道树形状压 \(\text{DP}\) 呢。

posted @ 2022-07-14 11:33  Hanx16Msgr  阅读(13)  评论(0)    收藏  举报