「CF1268D」Invertation in Tournament 题解

本文网址:https://www.cnblogs.com/zsc985246/p/17244884.html ,转载请注明出处。

传送门

「CF1268D」Invertation in Tournament

题目大意

给定一个竞赛图,一次操作可以将一个节点相连的所有边方向翻转。求让图强连通的最小操作次数。

竞赛图是一个无向完全图的每条边分配方向后的图。

思路

因为我们需要图 \(G\) 强连通,所以想到求出 scc 并缩点。

可以发现,我们进行翻转操作时,只有保证不破坏原有的 scc,才可以保证操作次数最小。

又因为我们能够合并 scc,我们肯定可以拆散 scc。

题目给了竞赛图的条件,思考怎么利用它。

不妨设我们现在有一个 scc,它的大小为 \(n\)

我们可以猜测,在 \(n \not= 3\) 时,存在一个点翻转之后不会拆散这个 scc。

Q:为什么 \(n \not= 3\)

A:\(n=3\) 时是一个单向环,显然不成立。

Q:为什么有反例我们还要坚持这条路?

A:如果只是 \(n=3\) 不满足条件,特殊处理不就好了。

\(n=1\) 显然不影响,竞赛图没有重边不存在 \(n=2\),我们只需要考虑 \(n \ge 4\) 的情况。

设我们用点 \(x\) 拆散了这个 scc,拆散过后 scc 个数为 \(m\),编号为 \(a_1,a_2,\dots,a_m\)

  • \(m=1\)

    根本就没有拆散,不用考虑。

  • \(m=2\)

    这时只有两类边,从 \(a_1\) 连向 \(a_2\) 的边和从 \(a_2\) 连向 \(a_1\) 的边。

    如果两类都存在,它们就会变成一个 scc,所以我们钦定只存在 \(2\) 类边。

    那么翻转之前如图,左边是 \(a_1\),右边是 \(a_2\)(没有展示所有的边):

    图挂了

    这时如果我们不翻转 \(x\),而是翻转 \(4\),那么红色 \(2\) 类边和除连接 \(x \to 4\) 外的橙色 \(1\) 类边就同时存在了。

    因为 \(n \ge 4\),scc 大小不为 \(2\),所以我们一定有一个 scc 大小大于等于 \(3\),那么翻转这个 scc 中的点就可以保证不拆散原来的 scc 了。

  • \(m \ge 3\)

    \(a_1,a_m\) 中有任意一个大小大于 \(1\) 时同 \(m=2\),只需将 \(a_m\) 看作 \(a_2\) 即可。

    那我们只需要考虑 \(a_1,a_m\) 大小都为 \(1\) 的情况。

    我们将 \(a_2,a_3,\dots,a_{m-1}\) 看作一个点 \(y\),如图:

    图挂了

    这时我们不翻转 \(x\),而是翻转 \(y\),就可以让任意 \(s,t\),有路线 \(s \to a_1 \to y \to a_2 \to t\)

    也就是说,我们选择 \(a_2,a_3,\dots,a_{m-1}\) 中的任意一个点翻转都不会拆散原来的 scc。

那到这里我们就证完了。

我们观察上面的证明可以发现,如果我们将过程逆转,从 拆散合并,同样是可以的。这提示我们,存在一个点使得翻转这个点之后图强连通。

那么 \(n\) 至少为多少?

  • \(m=1\)

    不用翻转。

  • \(m=2\)

    首先根据上面结论,一个大小大于等于 \(4\) 的 scc 才能够有至少一个点让我们翻转。那么只有当 \(n \ge 7\) 的时候才可以保证有一个 scc 大小大于等于 \(4\),所以 \(n\) 至少为 \(7\)

  • \(m \ge 3\)

    不难发现,在这种情况下,\(n\) 至少为 \(3\)

取上述最大值,\(n\) 至少为 \(7\)

那么 \(n \le 6\) 的情况呢?我们可以直接 \(2^n\) 暴力枚举翻转的点集即可。

最后,根据竞赛图的兰道定理,我们可以推出,一个竞赛图非强连通的充要条件是:将其顶点按出度从小到大排序,存在 \(k < n\) 使得前 \(k\) 个顶点的出度之和等于 \(C_k^2\)

这样我们就可以 \(O(n \log n)\) 完成一次判断,总复杂度 \(O(n^2 \log n)\)

代码实现

#include<bits/stdc++.h>
#define ll long long
#define For(i,a,b) for(ll i=(a);i<=(b);++i)
#define Rep(i,a,b) for(ll i=(a);i>=(b);--i)
const ll N=2e3+10;
const ll p=998244353;
using namespace std;

ll n;
ll minn=1e9,cnt;//最小操作次数和方案数量
ll a[N][N];//邻接矩阵

ll out[N];//出度
ll tmp[N];//出度
bool check(){//判断是否强连通
	//统计出度
	For(i,1,n)tmp[i]=out[i];//备份
	sort(tmp+1,tmp+n+1);//排序
	//判断强连通
	ll s=0;//前缀和
	For(i,1,n-1){
		s+=tmp[i];
		if(s==i*(i-1)/2)return false;
	}
	return true;
}

void rev(ll x){//翻转x
	For(i,1,n){
		out[x]-=a[x][i],out[i]-=a[i][x];
		a[x][i]^=1,a[i][x]^=1;
		out[x]+=a[x][i],out[i]+=a[i][x];
	}
}

namespace sub1{
	void dfs(ll x,ll ans){//暴力
		if(x>n){
			if(check()){
				if(ans<minn)minn=ans,cnt=1;
				else if(ans==minn)cnt++;
			}
			return;
		}
		dfs(x+1,ans);
		rev(x);
		dfs(x+1,ans+1);
		rev(x);
	}
	void solve(){
		dfs(1,0);
		if(minn==1e9)printf("-1");//无解
		else{
			For(i,1,minn)cnt=cnt*i%p;//可以打乱顺序
			printf("%lld %lld",minn,cnt);
		}
	}
}

namespace sub2{
	void solve(){
		if(check()){//答案可以为0
			printf("0 1");
			return;
		}
		For(i,1,n){
			rev(i);
			if(check())cnt++;
			rev(i);
		}
		printf("1 %lld",cnt);
	}
}

void mian(){
	
	scanf("%lld",&n);
	For(i,1,n){
		For(j,1,n){
			scanf("%1lld",&a[i][j]);
		}
	}
	For(i,1,n){
		For(j,1,n){
			out[i]+=a[i][j];//统计出度
		}
	}
	if(n<=6)sub1::solve();//暴力枚举
	else sub2::solve();//根据性质处理
	
}

int main(){
	int T=1;
//	scanf("%d",&T);
	while(T--)mian();
	return 0;
}

尾声

如果你发现了问题,你可以直接回复这篇题解

如果你有更好的想法,也可以直接回复!

posted @ 2023-03-22 22:43  zsc985246  阅读(207)  评论(1编辑  收藏  举报