[关闭]
@AkamemakA 2022-11-20T06:52:28.000000Z 字数 4039 阅读 100

CF733F Drivers Dissatisfaction 题解

题解 算法 杂题


题目大意:

给定一个 个点 条边的无向图,保证图联通。每条边有两个属性 ,表示使这条边的权值每降低 要花费 的花费。现在你有一个总费用 ,你可以降低某些边的权值(可以降为负数,且总费用不得超过 ) 。问降低权值后,这张图可得到的最小生成树权值是多少,并且输出选为最小生成树中的边的编号及权值。

解析

首先明确一点:题目中说可以降低某些边的权值,其实如果我们只选 最小的那条边,并只降低它的权值,这样一定可以使权值降低的最多(一点贪心的思想)。

我们可以首先找出这张图的最小生成树,并将在树上的边标记。然后,我们考虑降低哪条边的权值。

  1. 如果降低树边的权值

    其实非常好做。由开头所讲,只需要找到树边中 最小的那条边,将其 减少 即可。

  2. 如果降低非树边的权值

    这种情况稍微有些麻烦,我们来看下面这张图:

    黑色的边表示树边,而红色的这条边表示我们选择的要降低权值的这条边。

    由树的性质,我们不难发现:给树上两点间加上一条边后,便会生成一个环,由加上的边与两点间简单路径组成。要想其重新变回一棵树,那么必须从两点间的简单路径上去删除一条边。由贪心的思想,我们知道,要是最后答案最小,那么删去的边一定是两点间简单路径上权值最大的边。

    于是,我们可以枚举每一条非树边,找到其连接的两点的树上简单路径上权值最大的边,计算备选答案。

    备选答案就是 ,其中 表示原图最小生成树的权值和。

    对于找两点间权值最大的边,我们可以请出我们的树链剖分。码量虽然大,但是很好理解呀!

最后对于路径的输出(真恶心) , 把要选的边标记即可。有很多小细节,在代码里有呈现。本人蒟蒻,不喜勿喷!

  1. #include<iostream>
  2. #include<algorithm>
  3. using namespace std;
  4. #define int long long
  5. const int N=2e5+5;
  6. int n,m,S;
  7. int w[N],rec[N];
  8. int sum,rev;
  9. int he[N],ne[N<<1],go[N<<1],tot;
  10. int fa[N];
  11. struct node{
  12. int u,v,we,c,id;
  13. bool operator <(const node W) const{
  14. return we<W.we;
  15. }
  16. }a[N];
  17. bool st[N];
  18. inline void add(int a,int b){
  19. ne[++tot]=he[a];he[a]=tot;go[tot]=b;
  20. }
  21. inline int find(int x){
  22. if(x!=fa[x]) return fa[x]=find(fa[x]);
  23. else return x;
  24. }
  25. inline void kruskal(){//最小生成树模板
  26. int cnt=0;
  27. sort(a+1,a+m+1);
  28. for(int i=1;i<=m;i++){
  29. int A=find(a[i].u),B=find(a[i].v);
  30. if(A!=B){
  31. fa[B]=A;
  32. add(a[i].u,a[i].v);
  33. add(a[i].v,a[i].u);
  34. //建树
  35. cnt++;st[i]=1;sum+=a[i].we;
  36. //st[i]=1表示第i条边是一条树边
  37. //sum存储最小生成树的权值和
  38. }
  39. if(cnt==n-1) break;
  40. }
  41. }
  42. int top[N],dep[N],f[N],son[N],si[N];
  43. int dfn[N],nw[N],cnt;
  44. inline void dfs1(int u,int p){
  45. f[u]=p;dep[u]=dep[p]+1;
  46. si[u]=1;
  47. for(int i=he[u];i;i=ne[i]){
  48. int v=go[i];
  49. if(v==p) continue;
  50. dfs1(v,u);
  51. si[u]+=si[v];
  52. if(si[son[u]]<si[v]) son[u]=v;
  53. }
  54. }
  55. inline void dfs2(int u,int tp){
  56. top[u]=tp;
  57. dfn[u]=++cnt;
  58. nw[cnt]=w[u];
  59. if(son[u]) dfs2(son[u],tp);
  60. else return ;
  61. for(int i=he[u];i;i=ne[i]){
  62. int v=go[i];
  63. if(v==f[u]||v==son[u])continue;
  64. dfs2(v,v);
  65. }
  66. }
  67. struct Seg_Tree{
  68. int l,r;
  69. int val;
  70. #define lson (p<<1)
  71. #define rson (p<<1|1)
  72. }t[N<<2];
  73. inline void build(int p,int l,int r){
  74. t[p].l=l;t[p].r=r;
  75. if(l==r){
  76. t[p].val=nw[l];
  77. return ;
  78. }
  79. int mid=l+r>>1;
  80. build(lson,l,mid);
  81. build(rson,mid+1,r);
  82. t[p].val=max(t[lson].val,t[rson].val);
  83. }
  84. inline int query(int p,int l,int r){
  85. if(t[p].l>=l&&t[p].r<=r)return t[p].val;
  86. int res=0;
  87. if(t[lson].r>=l) res=max(res,query(lson,l,r));
  88. if(t[rson].l<=r) res=max(res,query(rson,l,r));
  89. return res;
  90. }
  91. inline int query_max(int x,int y){
  92. int res=0;
  93. while(top[x]!=top[y]){
  94. if(dep[top[x]]<dep[top[y]])
  95. swap(x,y);
  96. res=max(res,query(1,dfn[top[x]],dfn[x]));
  97. x=f[top[x]];
  98. }
  99. if(dep[x]>dep[y]) swap(x,y);
  100. res=max(res,query(1,dfn[son[x]],dfn[y]));
  101. //边权转点权,最后一部分的上点应取 son[x]
  102. return res;
  103. }
  104. //---------------以上均为模板---------------//
  105. signed main()
  106. {
  107. cin>>n>>m;
  108. for(int i=1;i<=n;i++) fa[i]=i;
  109. for(int i=1;i<=m;i++) cin>>a[i].we,a[i].id=i; //由于要输出边的序号,将其存进结构体
  110. for(int i=1;i<=m;i++) cin>>a[i].c;
  111. for(int i=1;i<=m;i++)
  112. cin>>a[i].u>>a[i].v;
  113. cin>>S;
  114. kruskal(); //最小生成树,同时建树
  115. rev=sum; //选非树边时方便得到备选答案,将最小生成树的权值拷贝一份
  116. dfs1(1,0);
  117. //由于题中给的是边权,我们可以将边权化为点权,用边连接的两点中深度更大的那个点的权值作为这条边的权值。
  118. for(int i=1;i<=m;i++){
  119. if(!st[i]) continue; //只处理树边
  120. if(dep[a[i].u]>dep[a[i].v]) w[a[i].u]=a[i].we,rec[a[i].u]=i;
  121. else w[a[i].v]=a[i].we,rec[a[i].v]=i;
  122. //w记录树中点权。rec表示该点向上连接的边的编号,输出时要用
  123. }
  124. dfs2(1,1);
  125. build(1,1,n);
  126. int minn=0x3f3f3f3f,id=0;
  127. //首先处理树边
  128. for(int i=1;i<=m;i++)
  129. if(st[i]){
  130. if(a[i].c<minn){
  131. minn=a[i].c;
  132. id=i;
  133. //minn找到树边中c[i]的最小值,id记录该边的编号
  134. }
  135. }
  136. int r=S/minn;
  137. sum-=r;//备选答案之一:选择树边
  138. bool flag=1; //由于两种情况在输出路径时的处理方式不同,因此记录改用哪种处理方式。
  139. int rd=0,cut=0;
  140. //如果选择了某条非树边比选择树边更优
  141. //那么flag标记为0,cut记录选择的那条边的权值,rd记录那条边的编号
  142. //蒟蒻想不出更多变量名了呜呜呜
  143. for(int i=1;i<=m;i++)
  144. if(!st[i]){
  145. int res=query_max(a[i].u,a[i].v);
  146. //找到两点简单路径中边权最大的边
  147. int r=S/a[i].c;
  148. if(rev-res+a[i].we-r<sum){ //解析中公式有呈现
  149. sum=rev-res+a[i].we-r;
  150. flag=0;rd=i;cut=res;
  151. //备选答案之二:选择非树边
  152. }
  153. }
  154. cout<<sum<<endl;
  155. if(!flag){ //如果选非树边更优
  156. int x=a[rd].u,y=a[rd].v;
  157. //由于query_max只能找到最大的数值而不能找到到底是那条边,因此枚举简单路径上的边即可
  158. while(1){
  159. if(dep[x]<dep[y]) swap(x,y);
  160. if(w[x]==cut) { //如果当前边边权为要删去的边的边权
  161. st[rec[x]]=0; //rec在这里派上用场:边权化为点权,x向上连接的那条边即为要删去的边
  162. st[rd]=1; //这条非树边标记
  163. int r=S/a[rd].c;
  164. a[rd].we-=r;
  165. break;
  166. }
  167. x=f[x]; //树剖的f和并查集的fa别搞混了(掉了几次坑的蒟蒻如是说)
  168. //每次向上跳一个
  169. }
  170. for(int i=1;i<=m;i++)
  171. if(st[i])
  172. cout<<a[i].id<<" "<<a[i].we<<endl; //sort会打乱边的顺序,因此输出a[i].id
  173. }else{
  174. //选择树边就很简单了,之间把要选的那条边的 w 减去 S/minn 即可
  175. int r=S/minn;
  176. a[id].we-=r;
  177. for(int i=1;i<=m;i++)
  178. if(st[i])
  179. cout<<a[i].id<<" "<<a[i].we<<endl;
  180. }
  181. }
添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注