【C++心路历程30】(APIO2013)道路费用

来源:互联网 发布:数控软件手机版 编辑:程序博客网 时间:2024/06/06 06:02

【问题描述】

  皮特现在是C国最富有的人。
  C国共有n个城市(用1~n 编号),现在这些城市由m条双向道路连接,其中城市1为首都。保证一个人从城市1出发,经过这些道路可以到达其他的任何一个城市。当然,所有的这些道路都是要收费的,使用道路i需要向该道路的所有者支付ci的费用。已知所有的ci互不相同。最近C国计划新建 条道路,毋庸置疑,当然是富豪皮特负责,因而新建的k条道路(也仅有这k条道路)是属于皮特的。
  皮特可以自行决定着k条道路的费用,并且皮特将在明天公布这些费用。两周以后,C国将在首都举行盛大的阅兵式,其中共有pi个参与者从城市i出发。大量的参与者将沿着这些道路前往首都。这些人只会沿着一个选出的道路集合行进,根据一个古老的习俗,这些道路将由最富有的人,也就是皮特指定,并且皮特将在后天公布选出的集合。同样根据这个习俗,皮特选出的道路集合必须使所有选出的道路的费用之和最小,并且仍要保证每个城市都可以经选出的道路到达首都。也就是说,选出的道路来自以费用作为相应边权的最小生成树。如果有多个这样的集合,皮特可以任选。
尽管皮特现在是首富,但他依旧想尽办法敛财。他希望通过控制属于他的 条道路的费用以及所选取的道路集合来使自己的收入最大化。他明白,他获得的收益并不只与指定的费用有关,也与通过这条道路的人数有关。准确的说,如果有p 个人经过费用为ci 的道路,那么道路所有者就会获得p*ci 的收入。注意,皮特的选择必须符合习俗。
  但是皮特并不够聪明,于是他来求助你。你现在需要做的就是计算皮特所能得到的最大收入!

【输入格式】

  第一行包含三个由空格隔开的正整数n,m,k 。
  接下来的m 行描述最开始的m 条道路。这ui,vi,ci 行每行包含三个空格隔开整数 ,表示在ui 和vi 之间有一条费用为ci 的双向道路。保证1<=ui,vi<=n ,且当i!=j 时,ci!=cj 。
  接下来k 行描述k 条新道路。每行包含两个空格隔开的整数ai,bi ,表示有一条连接城市ai,bi 的新道路,其费用由皮特决定。保证1<=ai,ni<=n 。
  最后一行包含n 个空格隔开的整数,其中第i 个表示pi ,即城市i 中要前往首都的人数。
保证在任意两个城市之间,最多有一条道路连接(包括新建的道路)。

【输出格式】

  输出只有一行,包含一个整数,表示皮特能获得的最大收入。

【输入样例】

5 5 1
3 5 2
1 2 3
2 3 5
2 4 4
4 3 6
1 3
10 20 30 40 50

【输出样例】

400

【样例解释】

  皮特应将新道路(1,3) 的费用设为5 。在这个费用下,他可以选择道路(3,5)、(1,3)、(2,4) 和 (1,3)。可以以证明,这样的收入是最大的。

大意:n个点,点权为Pi,m条边的图,新建不超过k条道路,每条费用由你决定,一条道路收取的费用为通过人数*边上的费用。但前提是所选边构成MST.在所有人都要去1号点集合的情况下,求你的最大收益。  
感觉题解网上其他地方似乎已经很多啦。所以就大概说一下。

暴力枚举1(期望得分20,过k=1的数据)(O(k*m))
将k条边逐一加入到原图最小生成树中,替换原图上的最大边。在k>1时不能保证正确。
核心代码:

`LL LCA(int u,int v){    int maxtt=-1;    if(dep[u]<dep[v])     {        swap(u,v);swap(xj[1].u,xj[1].v);    }    while(dep[u]!=dep[v])    {        if(maxtt<dist[u]-dist[fa[u]])        t1=u,t2=fa[u],maxtt=dist[u]-dist[fa[u]],aa=1;        u=fa[u];    }    while(u!=v)    {        if(maxtt<dist[u]-dist[fa[u]])        t1=u,t2=fa[u],maxtt=dist[u]-dist[fa[u]],aa=1;        u=fa[u];            if(maxtt<dist[v]-dist[fa[v]])        t1=v,t2=fa[v],maxtt=dist[v]-dist[fa[v]],aa=2;        v=fa[v];    }    LL A=maxtt;    return A;}void solve(){    LL ans=0;    LL sum=kruskal();    dfs(1,0,1,0);//给dfs做准备     LL cc=LCA(xj[1].u,xj[1].v);    if(aa==1) g[xj[1].v].push_back(xj[1].u);//这里比较蠢 有其他方法判断儿子/父亲    else g[xj[1].u].push_back(xj[1].v);    memset(fa,0,sizeof(fa));memset(dep,0,sizeof(dep));memset(dist,0,sizeof(dist));    dfs2(1,0);    if(aa==1) aa=sz[xj[1].u];    else aa=sz[xj[1].v];    ans=cc*aa;    cout<<ans;}`

暴力方法2(期望得分35)(注意到k<20,2^k枚举算法)(O(2^k*m*k))
对k条边枚举,共2^k种可能性:
对于每一种可能:
1:构造最终的最小生成树,优先使用选中的k条边
2:确定选中边的权值——将原图中还未选的边,加入进行LCA,则选中边边权为经过它的最小值。
核心代码:

void solve(){    memset(g,0,sizeof(g));    initial(n); int cnt=0;memset(used,0,sizeof(used));    for(int i=1;i<=k;i++)    if(vis[i]) //将选的边优先加入并查集     {        int u=xj[i].u,v=xj[i].v;        if(judge(u,v)) continue;        merge(u,v);cnt++;        g[u].push_back(v);        g[v].push_back(u);    }    sort(E+1,E+1+m);    for(int i=1;i<=m;i++)    {        int u=E[i].u,v=E[i].v;        if(judge(u,v)) continue;        merge(u,v);cnt++;        g[u].push_back(v);        g[v].push_back(u);        used[i]=1;        if(cnt==n-1) break;     }    memset(fa,0,sizeof(fa));memset(dep,0,sizeof(dep));    dfs(1,0,1);//给LCA做准备    for(int i=1;i<=k;i++) minv[i]=inf;    for(int i=1;i<=m;i++)    {        if(!used[i])        {            int u=E[i].u,v=E[i].v;LL c=E[i].c;            LCA(u,v,c);        }    }    memset(fa,0,sizeof(fa));memset(sz,0,sizeof(sz));    dfs2(1,0);    LL ans=0;    for(int i=1;i<=k;i++)    if(vis[i])    {        int u=xj[i].u,v=xj[i].v;        LL faa=min(sz[u],sz[v]);        ans+=faa*minv[i];    }    ANS=max(ans,ANS);}void run(int i,int r)//枚举2^k种可能方案 {    if(i>k)     {        if(r==0) return ;        solve();return ;    }       vis[i]=1;run(i+1,r+1);vis[i]=0;    run(i+1,r);}

算法三:
注意到原图中只有经过k条边才有可能收钱,即在其他边中无论怎么样移动都无关,即有些点是粘在一起的,与本题所求无关。即可以“缩点”。
怎么缩点?
在原图中,优先使用k条边生成MST,则k条边将原图分成k+1个连通块,每个连通块即可缩为一个点。(连通块中移动不干扰题目,并与k条边绝对无关)
再求这k+1个点的最小生成树,即原图压缩成只有(k+1)个点,k条边的新图。(这张图在本题所求中是与原图等价的,故而可以缩点),再通过算法二求解这张图即可。
时间复杂度:O(m*logm + 2^k*k*k)
核心代码:

void dfs3(int i,int f,int cc)//方便缩点{//g    belong[i]=cc,w[cc]=w[cc]+p[i];    for(int p=first[i];p;p=g[p].next)    {        int j=g[p].to;        if(j==f) continue;        dfs3(j,i,cc);    }}void yasuo(){    initial(n); int cnt=0;    for(int i=1;i<=k;i++) //将选的边优先加入并查集     {        int u=xj[i].u,v=xj[i].v;        if(judge(u,v)) continue;        merge(u,v);cnt++;    //  addedge(u,v,g);    //  addedge(v,u,g);//我要一棵假树!     }    sort(E+1,E+1+m);    for(int i=1;i<=m;i++)    {        int u=E[i].u,v=E[i].v;        if(judge(u,v)) continue;        merge(u,v);cnt++;        addedge(u,v,g);        addedge(v,u,g);        if(cnt==n-1) break;     }    for(int i=1;i<=n;i++)    if(!belong[i]) dfs3(i,0,++cc);    np=0,memset(first,0,sizeof(first));    kruskal2();}

P.S.
1.在枚举完边之后的操作中,如需要清空数组,请清空小数组(或新开小数组),原数组仍然很大,清空很慢!
2.可以剪枝的地方:在枚举2^k可能性时,如加入的k条边已经与原图构成环,可以跳出。
3.我自己程序足足写了250行,看起来就很冗杂+弱鸡,就不贴了……
4.感觉还是很有收获!