12894 Flipping Colors

Flipping Colors
时间限制: 6 Sec 内存限制: 128 MB

题目描述
You are given an undirected complete graph. Every pair of the nodes in the graph is connected by an edge, colored either red or black. Each edge is associated with an integer value called penalty.

By repeating certain operations on the given graph, a “spanning tree” should be formed with only the red edges. That is, the number of red edges should be made exactly one less than the number of nodes, and all the nodes should be made connected only via red edges, directly or indirectly. If two or more such trees can be formed, one with the least sum of penalties of red edges should be chosen.

In a single operation step, you choose one of the nodes and flip the colors of all the edges connected to it: Red ones will turn to black, and black ones to red.

在这里插入图片描述

Fig. F-1 The first dataset of Sample Input and its solution
For example, the leftmost graph of Fig. F-1 illustrates the first dataset of Sample Input. By flipping the colors of all the edges connected to the node 3, and then flipping all the edges connected to the node 2, you can form a spanning tree made of red edges as shown in the rightmost graph of the figure.

输入
The input consists of multiple datasets, each in the following format.

n
e1,2 e1,3 … e1,n-1 e1,n
e2,3 e2,4 … e2,n

en-1,n
The integer n (2 ≤ n ≤ 300) is the number of nodes. The nodes are numbered from 1 to n. The integer ei,k (1 ≤ |ei,k| ≤ 105) denotes the penalty and the initial color of the edge between the node i and the node k. Its absolute value |ei,k| represents the penalty of the edge. ei,k > 0 means that the edge is initially red, and ei,k < 0 means it is black.

The end of the input is indicated by a line containing a zero. The number of datasets does not exceed 50.

输出
For each dataset, print the sum of edge penalties of the red spanning tree with the least sum of edge penalties obtained by the above-described operations. If a red spanning tree can never be made by such operations, print -1.

样例输入
4
3 3 1
2 6
-4
3
1 -10
100
5
-2 -2 -2 -2
-1 -1 -1
-1 -1
1
4
-4 7 6
2 3
-1
0
样例输出
7
11
-1
9

题意

给定一张完全图,每条边有边权和颜色,负数表示黑色,正数表示红色。你可以进行任意次操作,每次选择一个点,反转与之相连所有边的颜色,求当所有红边形成一颗树(n-1条边且连通)时红边权值和的最小值。

做法

先分析这个操作的特性:

  1. 反转奇数次=反转一次,反转偶数次=没有操作,因此每个点只需要判断是否反转即可
  2. 所有点反转和所有点均不反转的结果相同,只需要考虑一次即可

因为最终求得的是一棵树,所以枚举每个点作为叶子节点的情况,再枚举剩下的n-1个点连接这个叶子节点的情况,此时由于叶子节点与其他所有点的连边都确定,叶子节点本身是否反转对图没有影响(因为如果叶子反转其他所有点也要跟着一起转,相当于大家都没转),不妨假设叶子反转,因为与叶子节点是否连边已经确定,所以每个点是否反转都是唯一确定的,我们就可以得到操作后的图,judge一下得到的图是否是一颗树就行了。这部分代码复杂度O(n2)。

一开始的judge直接用的邻接矩阵+并查集判是否有环,然后judge复杂度O(n2),虽然n是300,但是扛不过去多组输入…
优化judge的时候改写成了邻接表,只会搜到当前图中的n-1条红边(边数不是n-1的不需要judge,必然不是树),但是照着上面的情况重新建立邻接表的话总复杂度也会达到O(n4),不过不难发现对于相同的i,不同的j,邻接表也是有相同之处的,也就是反转j后邻接表都相同,因此可以通过这个相同的表反转j点得到进行judge的表:可以先把所有与i的边都转成黑色,将这张图建立一个邻接表a,然后反转j点,把a中和j无关的边加入邻接表b,并把与j点相连的边里a中不存在的边加入b中,后面这个操作只需要把a里存在的标记一下并且加入没标记的边就可以了。不难发现,一次更新操作最多增加/删除n-1个点,并且增删的点的个数的奇偶性必须与n-1相同。这样这种操作最多对边数为2n-2的邻接表进行遍历,对于每个j,建立邻接表的复杂度为O(n),judge复杂度也可以降到O(n),总复杂度o(n3)。

代码

#pragma GCC optimize(4)
#include <bits/stdc++.h>
#define rint register int
typedef long long ll;
using namespace std;
const int N=300+2;
const int inf=0x3f3f3f3f;
 
int n;
int c[N][N];
int v[N][N];
int fan[N];
int f[N];
int vv[N];
int ans=inf;

struct edge
{
    int u,v;
}vec[N*N],ggg[N*4];
int vn,gn;         //模拟vector 否则需要多次调用clear函数清空,直接用vector也可

int getf(int x)
{
    return x==f[x]?x:f[x]=getf(f[x]);
}
 
int main()
{
 
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    while(cin>>n&&n)
    {
        ans=inf;
        for(rint i=1;i<=n;++i)
            for(rint j=i+1;j<=n;++j)
            {
                cin>>v[i][j];
                if(v[i][j]<0) c[i][j]=c[j][i]=1,v[i][j]=v[j][i]=-v[i][j];
                else c[j][i]=c[i][j]=0,v[j][i]=v[i][j];
            }

        for(rint i=1;i<=n;++i)
        {
            for(rint j=1;j<=n;++j)
            {
                if(i==j) fan[i]=1;			//反转叶子节点
                else if(c[i][j]) fan[j]=1;	//反转其他节点使与叶子节点连边都为黑色
                else fan[j]=0;
            }
            //vec.clear();
            vn=0;
            for(rint j=1;j<=n;++j)
                for(rint k=1;k<=n;++k)
                    if(j<k&&((c[j][k]^fan[j]^fan[k])==0)) {	//在邻接表a中存边
                        vec[++vn].u=j;
                        vec[vn].v=k;
                        //vec.push_back({j, k, v[j][k]});
                    }
 
            if(vn<=2*(n-1)&&abs(vn-(n-1))%2==(n-1)%2)		//若不满足此条件必然不会得到树
            for(rint j=1;j<=n;++j)
            {
                if(i==j) continue;
                for(rint k=1;k<=n;++k) vv[k]=0;
                gn=0;//ggg.clear();
                for(rint k=1;k<=vn;++k)
                {
                    if(vec[k].u!=j&&vec[k].v!=j)			//在邻接表b中存边
                    {
                        ggg[++gn]=vec[k]; //ggg.push_back(it);
                    }
                    else
                    {
                        if(vec[k].u==j) vv[vec[k].v]=1;		//标记与j相连的边
                        else vv[vec[k].u]=1;
                    }
                }
 
                for(rint k=1;k<=n;++k)
                {
                    if(!vv[k]&&k!=j)						//将未相连的边存入邻接表b
                    {
                        ggg[++gn].u=j;
                        ggg[gn].v=k;
                    }
                }
                if(gn==n-1)			//judge
                {
                    for(rint k=1;k<=n;++k) f[k]=k;
                    int sum=0;
                    for(rint k=1;k<=gn;++k)
                    {
                        int x=getf(ggg[k].u);
                        int y=getf(ggg[k].v);
                        if(x==y) {sum=ans;break;}	//有环,不可更新答案。直接break会将ans更新成sum
                        else
                        {
                            f[x]=y;
                            sum+=v[ggg[k].u][ggg[k].v];
                            if(sum>=ans) break;
                        }
                    }
                    ans=min(ans,sum);
                }
            }
        }
        if(ans==inf) cout<<-1<<'\n';
        else cout<<ans<<'\n';
    }
    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值