【CCF 201909-5】城市规划(LCA+暴力全排列40 & 树形dp100) Apare_xzc

城市规划(ccf 201909-5)


题面:


思路

1. 我一看求树上的距离,就想到了很喜欢的LCA, 可以O(1)求距离,然后又看m个节点选K个,一看,我能不能暴力dfs出C(m,k)的全排列呢?计算一下复杂度,发现前4个测试点可以过掉,于是先开心地写一发巨长的暴力(大的数据sort瞎搞的,没骗到分)

LCA + dfs暴力枚举点
//用户名 试题名称 提交时间 代码长度 编程语言 评测结果 得分 时间使用 空间使用
//<许智超> 城市规划 11-22 10:59 3.180KB C0X 错误 40 3.078s 15.37MB
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e5+100;
const int maxm = 2e5+100;
int head[maxn],tot;
struct Node{
	int to,Next,d;
}node[maxm];
void initEdge()
{
	tot = 0; 
	memset(head,-1,sizeof(head));
}
void addedge(int from,int to,int d)
{
	node[tot].to = to;
	node[tot].d = d;
	node[tot].Next = head[from];
	head[from] = tot++;
}
int good[100500];
int First[maxn],cnt;
int dfn[maxn*2];
int deep[maxn*2];
int father[maxn];
int disToRoot[maxn];
int idd[maxn]; ///是干啥用的? 
bool vis[maxn];
bool cmp(int x,int y)
{
	return disToRoot[x]<disToRoot[y];
}
void dfs(int x,int dep)
{
	vis[x] = true;
	dfn[++cnt] = x;
	deep[cnt] = dep;
	First[x] = cnt;
	for(int i=head[x];i!=-1;i=node[i].Next)
	{
		int to = node[i].to;
		int d = node[i].d;
		if(vis[to]) continue;
		father[to] = x;
		disToRoot[to] = disToRoot[x]+d; 
		dfs(to,dep+1);
		dfn[++cnt] = x;
		deep[cnt] = dep;	
	}	
}
int ST[maxn][20];
int Log2[maxn]={-1};
void preRMQ(int m)
{
	int n = m*2-1;       
	for(int j=1;j<=n;++j) ST[j][0] = j;
	for(int i=1;(1<<i)<=n;++i)
	{
		for(int j=1;j+(1<<i)-1<=n;++j)
		{
			int a = ST[j][i-1], b = ST[j+(1<<(i-1))][i-1];
			ST[j][i] = deep[a]>deep[b]?b:a;
		}
	}
} 
int LCA(int xx,int yy)
{
	int x = First[xx], y = First[yy];
	if(x>y) swap(x,y);
	int i = Log2[y-x+1];
	int a = ST[x][i], b = ST[y-(1<<i)+1][i];
	int p = deep[a]>deep[b]?b:a;
	return dfn[p];
}
long long calDis(int px,int py)
{
	int fa = LCA(px,py);
	return disToRoot[px]-disToRoot[fa]-disToRoot[fa]+disToRoot[py];
}
int r[105000];
bool hasCelected[201900];
long long ans;
void solve(int k,int flag=0) //计算K个点两两之间的最短距离 
{
	long long sum = 0; 
	for(int i=1;i<=k;++i)
	{
		for(int j=i+1;j<=k;++j)
			sum += flag?calDis(r[i],r[j]):calDis(good[r[i]],good[r[j]]);
	}
	if(ans>sum) ans = sum;
}
void f1(int x,int M,int k) //C(M,k)
{
	if(x==k+1)
	{
		solve(k);
		return;
	}
	for(int i=r[x-1]+1;i<=M;++i)
	{
		if(hasCelected[i]) continue;
		hasCelected[i] = true;
		r[x] = i;
		f1(x+1,M,k);
		hasCelected[i] = false;
	}
}
int main()
{
	int n,m,k,NN,x,y,d;
	scanf("%d%d%d",&n,&m,&k);
	NN = n*2+10;
	for(int i=1;i<NN;++i)
		Log2[i] = Log2[i>>1]+1;
	for(int i=1;i<=m;++i)
		scanf("%d",good+i);
	initEdge();
	for(int i=1;i<n;++i)
	{
		scanf("%d%d%d",&x,&y,&d);
		addedge(x,y,d);
		addedge(y,x,d); 
	}
	cnt = 0;
	int root = good[1];
	father[root] = root;
	disToRoot[root] = 0; //root = 1 
	dfs(root,1);
	preRMQ(n);
	if((m<=2000&&k<=2)||(m<=16&&k<=16))  
	{
		ans = 1e16;
		r[0] = 0;
		f1(1,m,k);
		printf("%lld\n",ans);
	}
	else 
	{
		ans = 1e16;
		int upM = m;
		if(m>2000) upM = 2000;
		for(int kk=1;kk<=upM;++kk)
		{
			cnt = 0;
			for(int i=0;i<=n;++i) vis[i] = false;
			int root = kk;
			father[root] = root;
			disToRoot[root] = 0; //root = 1 
			dfs(root,1);
			preRMQ(n);	
			for(int i=1;i<=m;++i) idd[i] = good[i];
			sort(idd+1,idd+1+m,cmp);
			for(int i=1;i<=k;++i) r[i] = idd[i];
			solve(k,1);
		}
		printf("%lld\n",ans);
	}
	return 0;	
} 

思路:

2. 想不到什么好办法,于是去网上看题解,只有一个大牛写了题解,链接:https://blog.csdn.net/code92007/article/details/102022390

是树形DP,我觉得非常妙。而且这种2e3左右的很多都是N^2的DP

我们换个思路,不直接枚举点,而是计算每条边对于答案的贡献

———————————————————————————————————

dp[u][p] 表示的是以u为根节点的这棵树中,选取了p个点的距离之和的最小值
    要满足恰好选了K个点,那么如果以u为根节点的数中选了p个点,那么整个树
除了u这棵子树的所有树all-u就要选q个子节点
对于u的儿子节点v     u,v,w

令 tmp[p] = dp[u][p] 
先往下搜索子节点v,得到dp[v][...]的信息
父节点u和子节点v之间的边长为w
然后我们来更新dp[u][...]
最终的答案是dp[root][K]      

对于u->v:w 这条边来说
在还没有用v来更新u之前,子树v对于树u的贡献为0,
就是说现在的以u为根的树tree(u)相当于tree(u)去掉了以v为根节点的子树,即为tree(u-v)
如果要从子树tree(v)中选p个重要节点,那么tree(root-v)中就要选K-p个
那么状态是怎么转移的呢?
如果在tree(v)中选了x个点,在tree(root-v)中选了y个点,那么计算总距离的时候u->v:w这条
边对答案的贡献是 w * (x*y) 

/* 用户名 试题名称 提交时间 代码长度 编程语言 评测结果 得分 时间使用 空间使用 <许智超> 城市规划 11-23 10:41 2.626KB C0X 正确 100 140ms 21.63MB */
#include<bits/stdc++.h>
using namespace std;
const int maxn = 5e4+100;
int head[maxn],tot;
struct Node{
	int to,Next,d;
}node[maxn*2];
void initEdge()
{
	tot = 0;
	memset(head,-1,sizeof(head));
}
void addedge(int from,int to,int d)
{
	node[tot].to = to;
	node[tot].d = d;
	node[tot].Next = head[from];
	head[from] = tot++;
}
int dp[maxn][102],k;
int tree_u_v[102];
bool good[maxn];
int  num[maxn]; //num[u]表示以u为根节点的树中重要节点的个数 
//get到一种新的写法,对于树的遍历不用vis,只要遍历子节点的时候,if(v==fa) continue就好了 

/* dp[u][t] 的意义为: 以u为根节点的树中选择了t个重要节点(其它的所有节点中一共选了k-t个节点) 这棵树中的所有重要节点两两之间路径之和的最小值 */
void dfs(int u,int fa)   //对树进行先序遍历,在回溯的时候用子节点的信息去更新父节点的信息
{
	dp[u][0] = 0; //初始化,因为一个树一个重要节点都没选,点之间的路径之和当然为0
	//num[u] = 0;
	if(good[u]) //如果节点u是一个重要节点
	{
		dp[u][1] = 0; //那么从这棵树中选一个重要节点就选它自己,没有其他节点和它连接,最小值当然是0
		num[u] = 1; //记录以u为根节点的子树当前已知有一个重要节点
	} 
	for(int i=head[u];i!=-1;i=node[i].Next) //常规写法,遍历节点u所有可达的节点v(遍历所有的儿子节点v)
	{
		if(node[i].to==fa) continue;
		int v = node[i].to; //v代表节点u的儿子节点
		int d = node[i].d; //d代表u->v这条边的长度
		dfs(v,u); //先往下搜索子节点v

		//搜索完之后进行回溯,用dp[v][...]的信息更新dp[u][...]的信息
		for(int j=0;j<=k;++j)
			tree_u_v[j] = dp[u][j];  //先用tree_u_v[]这个数组当做一个临时变量区,存储被dp[v]更新之前的dp[u]
		int nU = min(k,num[u]); //因为最多选K个重要节点,所以可以取一个num[u]和K的最小值,避免多余的计算
		int nV = min(k,num[v]);//同上
		num[u] += num[v]; //以v为节点的树有num[v]个重要节点,然后用num[v]更新父节点u代表的树的重要节点数
		for(int j=0;j<=nU;++j) 
		{
			for(int t=0;t<=nV&&t+j<=k;++t)
			{
				dp[u][j+t] = min(1ll*dp[u][j+t],1ll*(k-t)*t*d+tree_u_v[j]+dp[v][t]); 
			}
		}
		/* 因为我们的目的是求选择的K个点之间两两的距离之和。我们要统计书上每一条边对于答案的贡献。 边的长度为d。我们用dfs先序遍历搜索,回溯的时候用子树v的信息来更新u的信息。tree_u_v[] 这个数组相当于是一个缓冲区,记录了用v更新之前u的信息。 dp[u][j+t] = min(1ll*dp[u][j+t],1ll*(k-t)*t*d+tree_u_v[j]+dp[v][t]); 这句代码其 实是在用u--->v这条边对于答案的贡献来更新dp[u][]的信息。 dp[v][t]代表的是以顶点v为根节点的子树含有v个重要节点的最小值 tree_u_v[] 中存储的是整个树减去v这棵树的信息。 如果用dp[v][t]来更新dp[u][], 那么以v为 根节点的子树中有t个重要节点,这些节点都在u->v这条边 的下方,而一共选k个节点,u->v这条边 的上方就有k-t个重要节点。 从这两个点集中各取一个点,两两之间的路径必定都含u->v这条边。 一共有 t * (k-t) 中组合。 所以 t * (k-t) * d, 就是用v更新u之后,边u->v:d 对答案增加的贡献。 我们用 (k-t)*t*d + tree_u_v[j] + dp[v][t] 来更新 dp[u][j+t] */
	}
}
int main()
{
	int n,m,x,y,d;
	scanf("%d%d%d",&n,&m,&k);
	for(int i=0;i<m;++i)
		scanf("%d",&x), good[x] = true;
	initEdge(); 
	for(int i=1;i<n;++i)
	{
		scanf("%d%d%d",&x,&y,&d);
		addedge(x,y,d);
		addedge(y,x,d);
	}
	memset(dp,0x3f,sizeof(dp));
	dfs(1,-1);
	printf("%d\n",dp[1][k]);
	
	return 0;
}


我爱树,我爱DP,我也爱LCA~

全部评论

相关推荐

点赞 收藏 评论
分享
牛客网
牛客企业服务