Codeforces1385 G. Columns Swaps(并查集2-sat)

题意:

给定一个2*n的矩阵,
一次操作你可以选择一列,交换该列的两行数字,每一列最多操作一次,
问最少操作多少次,能使得两行都是一个1到n的排列,
输出方案,如果无解输出-1。

数据范围:n<=2e5

解法:
因为题目输入的数都是[1,n]之间的,
那么先判断是否每种数都只出现两次,不为两次则无解.

如果两个相同的数字出现在同一行,
那么这个数字所在的两个列,一定有且只有一个要翻转.
对于每个列x,并查集节点x表示不翻转,x+n表示翻转,
如果两个列x和y必须翻转一个的话,合并x和y+n,合并x+n和y,
如果两个相同的数字补出现在同一行,
那么这个数字所在的两个列,要么都翻转要么都不翻转.
合并x和y,合并x+n和y+n
最后如果存在x和x+n在同一个集合,则无解,否则有解.

还要考虑如何输出方案,我对每个集合维护了一个链表,
输出的时候直接遍历链表即可.
因为要求操作次数最小,选择x和x+n中操作次数少的连通块即可.

ps:
2-sat问题的并查集解法?
code:
#include<bits/stdc++.h>
using namespace std;
const int maxm=4e5+5;
vector<int>g[2][maxm];
vector<int>temp[maxm];
vector<int>ans;
int head[maxm],nt[maxm],tail[maxm];
int a[2][maxm];
int mark[maxm];
int pre[maxm];
int cnt[maxm];
int n;
int ffind(int x){
    return pre[x]==x?x:pre[x]=ffind(pre[x]);
}
void add(int x,int y){
    x=ffind(x),y=ffind(y);
    if(x==y)return ;
    pre[y]=x;
    cnt[x]+=cnt[y];
    nt[tail[x]]=head[y];
    tail[x]=tail[y];
}
void cal(int x){
    for(int i=head[x];i;i=nt[i]){
        if(i>n){
            ans.push_back(i-n);
            mark[i-n]=1;
        }else{
            mark[i]=1;
        }
    }
}
signed main(){
    ios::sync_with_stdio(0);
    int T;cin>>T;
    while(T--){
        cin>>n;
        for(int i=1;i<=n;i++)cin>>a[0][i];
        for(int i=1;i<=n;i++)cin>>a[1][i];
        //判断数量合法性
        map<int,int>tot;
        for(int i=1;i<=n;i++){
            tot[a[0][i]]++;
            tot[a[1][i]]++;
        }
        int ok=1;
        for(auto i:tot){
            if(i.second!=2){
                ok=0;break;
            }
        }
        if(!ok){
            cout<<-1<<endl;
            continue;
        }
        //
        for(int i=1;i<=n;i++){
            g[0][i].clear();
            g[1][i].clear();
        }
        for(int i=1;i<=n;i++){
            g[0][a[0][i]].push_back(i);
            g[1][a[1][i]].push_back(i);
        }
        for(int i=1;i<=n;i++){//init
            pre[i]=i;
            cnt[i]=0;
            head[i]=i;
            tail[i]=i;
            nt[i]=0;
            //
            pre[i+n]=i+n;
            cnt[i+n]=1;
            head[i+n]=i+n;
            tail[i+n]=i+n;
            nt[i+n]=0;
        }
        for(int i=1;i<=n;i++){
            if(g[0][i].size()==2){//有且只有一个翻转
                int x=g[0][i][0],y=g[0][i][1];
                add(x,y+n);
                add(x+n,y);
            }else if(g[1][i].size()==2){//有且只有一个翻转
                int x=g[1][i][0],y=g[1][i][1];
                add(x,y+n);
                add(x+n,y);
            }else{//都翻转或都不翻转
                int x=g[0][i][0],y=g[1][i][0];
                add(x,y);
                add(x+n,y+n);
            }
        }
        for(int i=1;i<=n;i++){
            if(ffind(i)==ffind(i+n)){//即翻转又不翻转,无解
                ok=0;break;
            }
        }
        if(!ok){
            cout<<-1<<endl;
            continue;
        }
        ans.clear();
        for(int i=1;i<=n;i++){
            mark[i]=0;
        }
        for(int i=1;i<=n;i++){
            if(mark[i])continue;
            int x=ffind(i),y=ffind(i+n);
            if(cnt[x]<cnt[y]){
                cal(x);
            }else{
                cal(y);
            }
        }
        cout<<ans.size()<<endl;
        for(auto i:ans){
            cout<<i<<' ';
        }
        cout<<endl;
    }
    return 0;
}

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值