CF733F Drivers Dissatisfaction 题解

时间:2021-10-05
本文章向大家介绍CF733F Drivers Dissatisfaction 题解,主要包括CF733F Drivers Dissatisfaction 题解使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

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

0.闲聊几句

--->卑微的题面

博客食用更佳>-<


1.Solution

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

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

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

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

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

现在讨论每一条边,

  • 如果是在树上的边:

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

  • 如果是非树边:

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

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

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

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

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

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

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

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

2、看好题目中 \(ans\) 最大值可能为多少( \(2 \times 10^{14}\) !!!),所以用到 \(\text{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\n");
	//	}
	}
	if(flag)//防止除数为0
	a[flag].w = a[flag].w - (S / a[flag].c);
	printf("%lld\n",ans);
	for(int i = 1; i <= m; ++i){
		if(vis_new[i]){//如果这条边在新树中被标记过,就输出  
			printf("%lld %lld\n", 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错误,尽管提出

\(\text{The\ end.}\)

原文地址:https://www.cnblogs.com/Silymtics/p/15369775.html