• CF733F Drivers Dissatisfaction 题解


    一年前的题解,突然发现没搬过来/kx

    0.闲聊几句

    --->卑微的题面

    博客食用更佳>-<


    1.Solution

    主要思路:最小生成树 + ( ext{LCA}) + (感觉和严格次小生成树有点像)

    根据题意我们需要先用 ( ext{Kruskal}) 建出一棵最小生成树

    我们考虑枚举每一条边来确定降低那条边的权值更优。

    为什么只需要枚举一条边?

    假设现在有两条边 (e_1,e_2),如果 (e_1 > e_2) 那么不断的减 (e_2) 一定更优,这样赚便宜。如果 (e_1 = e_2) 那么减那条边都可以,既然都可以,把所有的操作都放在一个边上岂不是更简单?

    现在讨论每一条边,

    • 如果是在树上的边:

    直接把所有钱都砸在这条边上(money 的力量),看看得到的最小生成树权值和是否会更小

    • 如果是非树边:

    同样是把所有钱都砸在这条边上,这条边就会有一个新的权值。

    因为连接这条边会使原来的生成树出现一个环,那么需要我们删掉环上的一条边。根据贪心的策略,我们找到最大的边删除。然后判断此时生成树的权值和是否更小。如果更小就更新答案。

    直接枚举边做出判断是难以实现的,这就需要我们预处理最大值。

    倍增预处理或者直接裸上树链剖分都可以。

    再按照上面的思路跑一遍。

    最后输出答案(可能有多组解)。

    提几点注意事项(感谢Aliemo学姐为我debug):

    1、代码中有除法时要保证除数不为零,否则会RE,就像我

    RE一览

    2、看好题目中 (ans) 最大值可能为多少( (2 imes 10^{14}) !!!),所以用到 ( ext{INF}) 的时候要开大一点(开 (10^{18}) 就足够)

    3、注意不同图(树)中的边号所存的边号到底是排序前的边号,还是排序后的边号

    2.放代码

    我知道你们只喜欢这个

    2.1 关于变量名的解释:

    /*
    edge{
    	起点,终点
    	权值w,花费c,边号(排序后),下一条边 
    }e[]:生成树的边 
    ,a[]:输入的边 
    node{
    	边号,边的权值 
    } maxm[][]:用来存编号及边的权值(倍增里存的最大值 
    n:点数 
    m:边数
    S:钱数(花费
    ans:存答案 
    cnt:通过kruskal建出的生成树的权值和
    flag:标记减那一条边更优
    pre_jia:上一次加的边 
    pre_jian: 上一次减的边
    c[]:每条边减权值所需的花费
    w[]:每条边的权值
    fa[]:每个点的父亲(kruskal的并查集)
    f[][]:倍增惯用的小抄
    depth[]:每个点的深度(LCA惯用数组)
    vise[]:标记 用kruskal建完树 后有那几条边被选中 
    vis_new[]: 标记 用过减边权的边 后有那几条边被选中
    */
    

    2.2 真正的AC代码:

    #include<iostream>
    #include<cstdio>
    #include<algorithm>
    #define LL long long
    #define INF 1e18
    using namespace std;
    const int MAXN = 2e5+5;
    struct edge{
    	int from,to;
    	LL w,c,bh,nxt;
    	bool operator < (const edge &b) const {return w < b.w ;}
    }e[MAXN << 2],a[MAXN << 2];
    struct node{
    	int bh;LL dis;
    }maxm[MAXN][26];//maxm用来存编号及边的权值 
    int head[MAXN << 2], num_edge;
    LL n, m, S, ans, cnt, flag, pre_jia, pre_jian;
    int c[MAXN], w[MAXN]; 
    LL fa[MAXN];//并查集 
    LL f[MAXN][26];//倍增 
    LL depth[MAXN];//存深度 
    bool vise[MAXN << 1],vis_new[MAXN << 1];//vise标记在最小生成树中的边,vis_new标记更换过的边 
    
    LL find(LL x) {return fa[x] == x ? x : fa[x] = find(fa[x]); }
    void add(int from,int to,LL w,LL c, LL i){
    	e[++num_edge].from = from;		e[num_edge].to = to;
    	e[num_edge].w = w;				e[num_edge].c = c;
    	e[num_edge].bh = i;
    	e[num_edge].nxt = head[from];
    //	e[num_edge] = (edge){from, to, w, c, i, head[from]};
    	head[from] = num_edge;
    }
    
    void kruskal(){
    	for(int i=1;i<=m;++i){
    		LL uf = find(a[i].from), vf = find(a[i].to);
    		if(uf != vf){
    			add(a[i].from,a[i].to,a[i].w,a[i].c,i);
    			add(a[i].to,a[i].from,a[i].w,a[i].c,i);//加边是排序后的编号 
    			fa[uf] = vf;
    			cnt += a[i].w;
    			vise[i] = 1;
    			vis_new[i] = 1;
    		} 
    	}
    }
    
    void dfs(LL u,LL fa){//在建出的最小生成树上跑DFS,并对LCA进行初始化 
    	f[u][0] = fa;
    	//cout<<u<<" "<<fa<<"lzx"<<endl;
    	for(int i=head[u]; i; i = e[i].nxt){
    		LL v = e[i].to;
    		if(v == fa) continue;
    		else{
    			depth[v] = depth[u] + 1ll;
    			maxm[v][0].dis = e[i].w;
    			maxm[v][0].bh = e[i].bh;//a中排序后的编号 
    			dfs(v,u);
    		}
    	}
    }
    
    void init_lca(){
    	for(int i=1;i<=25;++i){
    		for(int j = 1; j <= n; ++j){
    		//cout<<f[j][0]<<"fjh"<<endl;
    			f[j][i] = f[f[j][i-1]][i-1];//LCA初始化 
    			if(maxm[j][i-1].dis > maxm[f[j][i-1]][i-1].dis){
    				maxm[j][i].dis = maxm[j][i-1].dis;//存向上跳2^i步的最大值 
    				maxm[j][i].bh = maxm[j][i-1].bh;//及编号 
    			}
    			else{
    				maxm[j][i].dis = maxm[f[j][i-1]][i-1].dis;
    				maxm[j][i].bh = maxm[f[j][i-1]][i-1].bh;
    			}
    		}
    	}
    }
    
    LL get_lca(LL u,LL v){
    	if(depth[u] > depth[v]) swap(u,v);//保证v的深度大于u的深度 
    	for(int i = 25; i >= 0; --i){//把v点调到和u点同样的位置 
    		if(depth[f[v][i]] < depth[u]) continue;
    		else v = f[v][i];
    	}
    	if(u == v) return u;//如果此时是同一个点,直接返回即可 
    	for(int i = 25; i >= 0; --i){//两个点一起向上跳,跳到最近公共祖先的下面 
    		if(f[v][i] != f[u][i]){ 
    			v = f[v][i];
    			u = f[u][i];
    		}
    	}
    	//cout<<u<<"wzd"<<endl;
    	return f[u][0];//再跳一下 
    }
    
    node qmax(LL u, LL v){
    	node Ans;
    	Ans.bh = -1;	Ans.dis = -INF;
    	for(int i = 25; i >= 0; --i){
    		if(depth[f[u][i]] >= depth[v]){
    			if(Ans.dis < maxm[u][i].dis){//如果珂跳到最近公共祖先的下面,尝试更新最大值 
    				Ans.dis = maxm[u][i].dis;
    				Ans.bh = maxm[u][i].bh;
    			}
    			u = f[u][i];//向上跳一下 
    		}
    	}
    	return Ans;//返回Ans值 
    }
    
    int main()
    {
       	scanf("%lld%lld",&n,&m);
    	for(int i=1;i<=m;++i)	scanf("%d",&w[i]);
    	for(int i=1;i<=m;++i)	scanf("%d",&c[i]);
    	for(int i=1,u,v;i<=m;++i){//存边 
    		scanf("%d%d",&u,&v);
    		a[i].from = u;	a[i].to = v;
    		a[i].w = w[i];	a[i].c = c[i];
    		a[i].bh = i;//a[i].bh存的是一开始的边号 
    	}
    	scanf("%lld",&S);
    	
    	for(int i=1;i<=n;++i) fa[i] = i;
    	sort(a+1,a+m+1);//i与a[i].bh已经不一样了 
    	kruskal();
    	
    	depth[1] = 1;
    		maxm[1][0].dis = -INF;
    		maxm[1][0].bh = e[1].bh;
    	
    	dfs(1,1);//深搜初始化LCA 
    	init_lca();//初始化LCA
    	
    	 
    	ans = INF;
    	for(int i=1;i<=m;++i){
    		LL ww = S / a[i].c;
    		if(vise[i]){//如果这条边是最小生成树里面的,直接减去即可 
    			if(ans > cnt - ww){
    				flag = i; //更新要减权值的bian 
    			} 
    			ans = min(ans, cnt - ww);
    		}
    		else{
    			LL u = a[i].from, v = a[i].to;
    			LL lca = get_lca(u,v);//u,v的LCA 
    			node maxu = qmax(u, lca);//两边的最大值都要求 
    			node maxv = qmax(v, lca);
    			if(maxu.dis > maxv.dis){//挑有最大边最大的那一边 
    				if(ans > cnt - maxu.dis + a[i].w - ww){
    		//删掉最长的那个边,加上新来的那条边,减去可以减得边权值 
    					ans = cnt - maxu.dis + a[i].w - ww;
    					vis_new[pre_jia] = 0;
    					vis_new[pre_jian] = 1;
    					vis_new[i] = 1;//把新加的标记一下 
    					vis_new[maxu.bh] = 0;//把删掉的标记为0 
    					pre_jia = i;
    					pre_jian = maxu.bh;//这里都是a中排序后的边号 
    					flag = i;//更新一下标记bian
    				}	
    			}
    			else{
    				if(ans > cnt - maxv.dis + a[i].w - ww){
    					ans = cnt - maxv.dis + a[i].w - ww;
    					vis_new[pre_jia] = 0;
    					vis_new[pre_jian] = 1;
    					vis_new[i] = 1;
    					vis_new[maxv.bh] = 0;
    					pre_jia = i;
    					pre_jian = maxv.bh;//这里也是排序后的边号 
    					flag = i;//更新一下标记bian
    				}
    			}
    		}
    		//if(pre_jian == 1){
    	//		cout<<"lkp"<<endl;
    	//	}
    	//	if( vis_new[1] == 0){
    	//		printf("bj
    ");
    	//	}
    	}
    	if(flag)//防止除数为0
    	a[flag].w = a[flag].w - (S / a[flag].c);
    	printf("%lld
    ",ans);
    	for(int i = 1; i <= m; ++i){
    		if(vis_new[i]){//如果这条边在新树中被标记过,就输出  
    			printf("%lld %lld
    ", a[i].bh, a[i].w);
    		}
    	}
    	
    	return 0;
    }
    

    2.3 附:我遇到的多组解:

    样例一题面输出:

    0
    1 1
    3 1
    6 1
    7 2
    8 -5
    

    我的输出(不同但是过了):

    0
    1 1
    4 1
    6 1
    7 2
    8 -5
    

    手膜样例之后发现其实都是一组合法解,需要自己注意一下。

    样例不对不要慌,万一交上去AC了呢


    3.写在最后

    第一次写黑题题解(现在掉紫了),由于细节太多,难免有些遗漏

    如果您有什么疑惑,或我犯了什么sb错误,尽管提出

    ( ext{The end.})

  • 相关阅读:
    4.2编写第一个servlet(500错误解决)超级开心
    springmvc文件上传下载
    找出字符串中不重复字符的最长子串的长度
    微信小程序接入,https服务器搭建和调试
    js常用写法
    通过条件删除标签
    xml的修改遍历,以及建立
    shelve模块
    dump与load
    locate,find,df,mount,du命令
  • 原文地址:https://www.cnblogs.com/Silymtics/p/solution-CF733F.html
Copyright © 2020-2023  润新知