#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}\) 呢。