SRM537-div1-3-PrinceXDominoes


题目大意:
     有若干张多米诺牌,每张牌两端分别用红色和黑色写两个整数,如果两张牌上不同颜色的数字相同,那么可以把相应的数字连接起来。现用String[] dominoes表示所有的多米诺牌,牌上数字的取值范围为[0,m-1], 其中 dominoes[i].charAt(j)表示红色数字为i,黑色数字为j的牌数,如果为'.',表示牌数为0,否则取值为'A'到'Z'之间,表示1到26张。问:用这些牌最大可以组成多大的一个环,要求每一种牌至少被使用一次,否则返回-1。
     数据规模:dominoes数组大小(即m)为[2,30]
     

思路:
     题目所要求的找一个最大的环,等价于去掉最少的无用的牌,然后剩下的牌可以组成一个环。所以,需要研究一组牌在什么情况下才可以连接成一个环。对于数字x,如果红色x牌与黑色x牌的数量不相等,则显然无法组成一环,必须去掉多余的那部分。由于一张牌有两种不同颜色的数字,去掉一组之后又会影响其他数字的情况,这个过程与网络流中求最大流的过程有点像,所以考虑使用网络流来求解。
     按照题中的连接方式,可以构建一个拥有m个顶点的网络,m个顶点分别表示m个数字,红色i黑色j的牌表示成一条由i指向j的边,边的容量为牌数。如果使用了y张红i黑j的牌,那么边ij的流量为y。
     那么,一组牌能够组成一个环等价于所表示的流可以分成若干个不完全独立的环流。所以,我们的目标是在该网络中找到一系列不完全独立的环流,使得所有边的流量总和最大化。(不完全独立的环流:如果每一个环流看作一个顶点,两个环流拥有相同数字则连一条边,那么所构成的图是一个连通图。)
     可以通过以下方法找到这样的一组环流:为网络增加一个源点(source)和一个终点(sink),如果红色x牌数>黑色x牌数,则增加一条source到x的边,容量为牌数差;如果红色x牌数<黑色x牌数,则增加一条x到sink的边,容量为牌数差。显然,由source出发的边的容量和等于指向sink的边的容量和,假设为f。则在该网络中可以寻找到一组从source到sink并且值为f的网络流,所有边的剩余容量可以组成一系列的环流。这个过程类似于寻找一组不使用的牌,剩下的牌可以组成一个环。由于牌总数是固定的,只要弃用的牌最少,那么环就最大。
     因此,可以将该问题转换成一个source到sink的最小费用最大流问题,每条边的费用为1。最大环的牌数=总牌数-该最小费用。
     除此之外,还必须注意以下两点:
  • 题目中要求每种牌至少被使用一次,每一种牌被弃用的牌数最多只能为其牌数-1。所以,以上网络中不与source和sink连接的所有边的容量都必须减1。如果经此修改后的网络的最大流无法达到f,则说明必须完全弃用某种牌,应该返回-1.
  • 我们所要找的一系列环流必须是非完全独立的,所以首先必须要确保这些数字组成的网络是一个强连通图。不过不是强连通图则返回-1。
     以下代码使用最小费用路算法来求解最小费用最大流问题。由于最大流不会超过13*m,所以最多只需要寻找13*m次最小费用路。寻找最小费用路使用SPFA算法,算法复杂度为O(m^2),因此算法总复杂度为O(m^3)。这里寻找最小费用路也可以使用Bellman-Ford算法,最终算法复杂度会达到O(m^4),应该也可以过。


Java代码:
public class PrinceXDominoes {
    public int play(String[] dominoes) {
        int m = dominoes.length;
        int[] delta = new int[m];//每个数字的红黑数差值
        int dNum = 0;//牌总数
        boolean[][] connected = new boolean[m][m];
        for(int i = 0; i < m; ++i){
            connected[i][i] = true;
        }
        //graph[0]为原图,graph[1]为residual图
        int[][][] graph = new int[2][m + 2][m + 2];
        for(int i = 0; i < m; ++i) for(int j = 0; j < m; ++j){
            if(dominoes[i].charAt(j) != '.'){
                int cap = dominoes[i].charAt(j) - 'A' + 1;
                dNum += cap;
                connected[i][j] = true;
                graph[0][i][j] = cap - 1;
                if(i != j){
                    delta[i] += cap;
                    delta[j] -= cap;
                }
            }
        }
        //Floyd Warshall
        for(int k = 0; k < m; ++k) for(int i = 0; i < m; ++i) for(int j = 0; j < m; ++j){
            connected[i][j] = connected[i][j] || (connected[i][k] && connected[k][j]);
        }
        //如果非完全连通则返回-1
        for(int i = 0; i < m; ++i) for(int j = 0; j < m; ++j) if(!connected[i][j]){
            return -1;
        }
        //红黑差值为正的与source连接,为负的与sink连接
        for(int i = 0; i < m; ++i){
            if(delta[i] > 0){
                graph[0][m][i] = delta[i];
            }else{
                graph[0][i][m + 1] = -delta[i];
            }
        }
        //利用最小费用流寻找最少弃用的牌数
        int cost = maxFlowMinCost(graph);
        if(cost < 0){
            return -1;
        }else{
            return dNum - cost;
        }
       
    }
   
    private int maxFlowMinCost(int[][][] graph){
        int[] pre = null;
        int n = graph[0].length;
        int[] cost = new int[1];
        while((pre = SPFA(graph)) != null){
            int cap = augment(graph, n - 1, pre, Integer.MAX_VALUE, cost);
            //与source和sink连接的边cost应该为0,在这里减去
            cost[0] -= 2 * cap;
        }
        for(int i = 0; i < n; ++i) if(graph[0][n - 2][i] != 0){
            //没有达到最大流,说明需要弃掉某些唯一的牌,不满足条件,返回-1
            return -1;
        }
        return cost[0];
    }
   
    private int augment(int[][][] graph, int v, int[] pre, int cap, int[] cost){
        int u = pre[v];
        int r = graph[1][u][v] > 0 ? 1 : 0;
        int minCap = Math.min(cap, graph[r][u][v]);
        if(u != graph[0].length - 2){
            minCap = augment(graph, u, pre, minCap, cost);
        }
        graph[r][u][v] -= minCap;
        graph[1 - r][v][u] += minCap;
        cost[0] += (r == 0 ? 1 : -1) * minCap;
        return minCap;
    }
    private int[] SPFA(int[][][] graph){
        int n = graph[0].length;
        int[] dist = new int[n];
        int[] pre = new int[n];
        Arrays.fill(pre, -1);
        Arrays.fill(dist, Integer.MAX_VALUE);
        dist[n - 2] = 0;
        boolean[] in = new boolean[n];
        int[] queue = new int[n];
        int head = 0, tail = 0;
        queue[tail++] = n - 2;
        in[n - 2] = true;
        while(head != tail){
            int u = queue[head];
            head = (head + 1) % n;
            in[u] = false;
            for(int v = 0; v < n; ++v){
                for(int r = 0; r < 2; ++r){
                    if(graph[r][u][v] > 0 && dist[v] > dist[u] + (r == 0 ? 1 : -1)){
                        dist[v] = dist[u] + (r == 0 ? 1 : -1);
                        if(!in[v]){
                            in[v] = true;
                            queue[tail] = v;
                            tail = (tail + 1) % n;
                        }
                        pre[v] = u;
                    }
                }
            }
        }
        if(pre[n - 1] != -1){
            return pre;
        }else{
            return null;
        }
    }
}



  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值