「CF1268D」Invertation in Tournament 题解
本文网址:https://www.cnblogs.com/zsc985246/p/17244884.html ,转载请注明出处。
传送门
「CF1268D」Invertation in Tournament
题目大意
给定一个竞赛图,一次操作可以将一个节点相连的所有边方向翻转。求让图强连通的最小操作次数。
竞赛图是一个无向完全图的每条边分配方向后的图。
思路
因为我们需要图 强连通,所以想到求出 scc 并缩点。
可以发现,我们进行翻转操作时,只有保证不破坏原有的 scc,才可以保证操作次数最小。
又因为我们能够合并 scc,我们肯定可以拆散 scc。
题目给了竞赛图的条件,思考怎么利用它。
不妨设我们现在有一个 scc,它的大小为 。
我们可以猜测,在 时,存在一个点翻转之后不会拆散这个 scc。
Q:为什么 ?
A: 时是一个单向环,显然不成立。
Q:为什么有反例我们还要坚持这条路?
A:如果只是 不满足条件,特殊处理不就好了。
显然不影响,竞赛图没有重边不存在 ,我们只需要考虑 的情况。
设我们用点 拆散了这个 scc,拆散过后 scc 个数为 ,编号为 。
-
根本就没有拆散,不用考虑。
-
这时只有两类边,从 连向 的边和从 连向 的边。
如果两类都存在,它们就会变成一个 scc,所以我们钦定只存在 类边。
那么翻转之前如图,左边是 ,右边是 (没有展示所有的边):
这时如果我们不翻转 ,而是翻转 ,那么红色 类边和除连接 外的橙色 类边就同时存在了。
因为 ,scc 大小不为 ,所以我们一定有一个 scc 大小大于等于 ,那么翻转这个 scc 中的点就可以保证不拆散原来的 scc 了。
-
当 中有任意一个大小大于 时同 ,只需将 看作 即可。
那我们只需要考虑 大小都为 的情况。
我们将 看作一个点 ,如图:
这时我们不翻转 ,而是翻转 ,就可以让任意 ,有路线 。
也就是说,我们选择 中的任意一个点翻转都不会拆散原来的 scc。
那到这里我们就证完了。
我们观察上面的证明可以发现,如果我们将过程逆转,从 拆散
到 合并
,同样是可以的。这提示我们,存在一个点使得翻转这个点之后图强连通。
那么 至少为多少?
-
不用翻转。
-
首先根据上面结论,一个大小大于等于 的 scc 才能够有至少一个点让我们翻转。那么只有当 的时候才可以保证有一个 scc 大小大于等于 ,所以 至少为 。
-
不难发现,在这种情况下, 至少为 。
取上述最大值, 至少为 。
那么 的情况呢?我们可以直接 暴力枚举翻转的点集即可。
最后,根据竞赛图的兰道定理,我们可以推出,一个竞赛图非强连通的充要条件是:将其顶点按出度从小到大排序,存在 使得前 个顶点的出度之和等于 。
这样我们就可以 完成一次判断,总复杂度 。
代码实现
#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;
}
尾声
如果你发现了问题,你可以直接回复这篇题解
如果你有更好的想法,也可以直接回复!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现