zoukankan      html  css  js  c++  java
  • UVA 10806 最小费用最大流

    终于可以写这道题的题解了,昨天下午纠结我一下下午,晚上才照着人家的题解敲出来,今天上午又干坐着想了两个小时,才弄明白这个问题。

    题意很简单,给出一个无向图,要求从1 到 n最短路两次,但是两次不允许经过同一条边(正反都不能经过),如果能成功,则输出两次的最小长度。否则输出一句话。

    我当时就马上敲了一个最短路,执行两次,在第一次执行完之后就把所经过的路径的正反都锁定好,不允许下次再访问。。。这样做通过了sample,但是WA了,我后来上网查,原来好多人是我这种做法,。。而且原来这是最小费用最大流问题。

    分析一下为什么我的做法不行,因为这道题目要求在能通过的前提下,最短,也就是说,我连续两次最短路,如果第二次不能通过,就放弃,这明显是错的,最终的结果,也许是一条最短路+一条长路,或者两条都不是最短路。。。

    所以一种可行的修改方案是,在原来的代码基础上,不是直接封锁正反两条路,而是封锁正向,把反向边=权值的相反数,为什么这样的,简而言之,就是给出反悔的机会

    我们来思考这样一种实际的例子 如果 path1:起点-p1-A-B-p2-终点; path2:起点-p3-B-A-P4-终点;p1 p2 p3 p4都是互不干扰的路径,这样的话,其实两条路径就是p1-p3

    p2-p4,A-B作为公共边,在程序里面最短路确实是这样通过的,但实际上由于权值取反,正好抵消,使得A-B B-A实际意义上是没有被经过,但是路径是通的,这样就能够智能的去选择路径方案。

    最终我是用最小费用最大流来解决的,通过这个题目,我对最大流有了更深刻的理解,明白了残量图的更大的作用,为什么当时E-K算法里面,找到增广路后需要对正向流进行扩展,同时又对反向流减少,这其实就是对应了现实的流的概念,我递增正向,但是流是守恒的,我可以通过反向把流又送回来,因此用这个方法,使得最大流不会放过任何一个能走通的路径,在E-K算法里,其实流经过了多次往返,只有确定无法走通,才会放弃。。。同时在建图的时候,因为是无向图,所以每条边,都要建立一个相应的负费用边。。。这样的话每个端点实际上连了两条边,一个是自己的正费用边,一个是对面端点的负费用边,我一开始会觉得这样在最短路过程中会不会发生混乱,即这个点往下走,到底是走的自己本身的正费用边,还是负费用边,后来不管费用如何,最短路最终会选择最优的那条,因此不会有问题

    因为涉及边数多,所以只能有邻接链表,以及spfa来做。

    #include <iostream>
    #include <cstdio>
    #include <cstring>
    #include <queue>
    using namespace std;
    struct node
    {
        int u,v,w,flow,cab,nt;
    } e[50000];;8 
    int ft[110],d[110],inq[110],p[110];
    int cnt,n,m;
    int cc,ff;
    void add(int u,int v,int cost,int cab)
    {
        e[cnt].u=u;   //对每个点添加一条正费用边,以及下面的负费用边
        e[cnt].v=v;
        e[cnt].w=cost;
        e[cnt].cab=cab;
        e[cnt].flow=0;
        e[cnt].nt=ft[u];
        ft[u]=cnt++;
    
        e[cnt].u=v;
        e[cnt].v=u;
        e[cnt].w=-cost;
        e[cnt].cab=0;
        e[cnt].flow=0;
        e[cnt].nt=ft[v];
        ft[v]=cnt++;
    }
    void ek()
    {
        queue<int> q;
        cc=ff=0;
        int i,j;
        for (;;)
        {
            for (i=0;i<=n+1;i++)
                d[i]=1<<30;
            d[0]=0;
            memset(inq,0,sizeof inq);
            memset(p,-1,sizeof p);
            q.push(0);
            while (!q.empty()) //SPFA搜最短路
            {
                int u=q.front();
                q.pop();
                inq[u]=0;
                for (j=ft[u];j>=0;j=e[j].nt)
                {
                    int nx=e[j].v;
                    if (e[j].cab>e[j].flow && d[nx]>d[u]+e[j].w)
                    {
                        d[nx]=d[u]+e[j].w;
                        p[nx]=j;  //这里不能记录前端点,因为端点指向的边不止一点,因此只能记录该点对应的是哪条边
                        if (!inq[nx])
                        {
                            q.push(nx);
                            inq[nx]=1;
                        }
                    }
                }
            }
            if (d[n+1]>=(1<<30)) break;
            int a=1<<30;
            for (i=p[n+1];i>=0;i=p[e[i].u])
            {
                if (a>e[i].cab-e[i].flow)
                    a=e[i].cab-e[i].flow;
            }
            for (i=p[n+1];i>=0;i=p[e[i].u])
            {
                e[i].flow+=a;
                e[i^1].flow-=a;//用异或可以使得当前i很快找到对应的那条负费用边 或者 正费用边,因为添加边的时候都是两条两条的加。
            }
            cc+=d[n+1]*a;
            ff+=a;
        }
    }
    int main()
    {
        int i,j;
        while (scanf("%d",&n))
        {
            if (n==0) break;
            cnt=0;
            scanf("%d",&m);
            int a,b,c;
            memset(ft,-1,sizeof ft);
            for (i=1;i<=m;i++)
            {
                scanf("%d%d%d",&a,&b,&c);
                add(a,b,c,1);
                add(b,a,c,1);
            }
            add(0,1,0,2); //添加超级原点和终点,这样就方便最后的答案计算。
            add(n,n+1,0,2);
            ek();
            if (ff>=2)
                printf("%d
    ",cc);
            else
                puts("Back to jail");
        }
        return 0;
    }
  • 相关阅读:
    ZJU 1610
    zju1484
    字符串赋值与初始化
    内核线程、内核级线程(轻量级进程)和用户级线程
    Mysql基础
    结构体的sizeof
    对象属性值读取问题
    返回引用类型
    操作符重载为成员函数、非成员函数与友元函数的区别
    运算符优先级
  • 原文地址:https://www.cnblogs.com/kkrisen/p/3522348.html
Copyright © 2011-2022 走看看