• 【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
    ",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
    ",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
    ",dp[1][k]);
    	
    	return 0;
    }
    
    

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

  • 相关阅读:
    SpringBoot集成Redis
    独享锁 & 共享锁
    公平锁与非公平锁
    如何上传本地代码到码云
    SpringBoot+Mybatis+Pagehelper分页
    SpringBoot集成Mybatis(0配置注解版)
    高并发下接口幂等性解决方案
    SpringBoot全局配置文件
    干货
    Spring读取外部的资源配置文件—@PropertySource和@Value实现资源文件配置
  • 原文地址:https://www.cnblogs.com/Apare-xzc/p/12243633.html
Copyright © 2020-2023  润新知