1. 定义
竞赛图,即任意两点之前有且仅有一条边的有向图。即有向完全图,有(2n) 条边。
至于为什么叫竞赛图,就是赢得点向输的点连边,一个边代表的是胜负关系。
2. 性质
兰道定理(竞赛图判定)
我们定义比分序列为将每个点的出度si 从小到大排序的序列。
那么若满足i=1∑ksi≥(2k) 且当k=n 时取等(即∑s=(2n)),则一定能够造出一种竞赛图,反之不能。
必要性显然,考虑充分性证明,我们构造初始图,对于j<i 则i→j 连边,设此时比分序列为a,这个序列显然在上述条件能够取到等号。保持i=1∑kai≤i=1∑ksi,不断调整图直到a=s。
未构造完成时开头必然是一段等于后面接一个al<sl,为了让后面的方法修改后使得a 仍然有序,我们找到最后一个al=au 的u,显然有au<su。因为总和固定,必然能找到第一个v 使得av>sv。
此时有au<su≤sv<av,有av≥au+2。
当∃v→u,考虑翻转这条边,否则必然存在p 使得v→p→u,那么把路径反转。因为au<su,ai≤si,∀i∈(u,v),不难证明反转后的序列仍然保持性质(∑a≤s)这样构造下去一定有解。
注意,这里定理都是存在,对于同一个比分序列可能对应本质不同的竞赛图。
兰道定理实质
兰道定理的实质是在一个序列和一定时,可以对于任意两个有关系(边)的位置进行相等量的调整(si←si+v,sj←sj−v),那么要判定任意一个值的序列是否与序列值的下界形式一样时,只需要判断是否每个前缀和都大于等于下界,以及最后的和与预期相等即可。证明考虑构造法不断调整。
竞赛图强连通分量个数(推论)
即:
i=1∑n[j=1∑isj=(2i)]
其中s 还是比分序列。证明如下:
G 缩点后形成了一条链,拓扑序上靠后的强连通分量里节点的出度一定严格小于靠前的强连通分量里节点的出度。考察链上相邻两个强连通分量S,T,不妨假设S 在拓扑序上比T 靠前。我们一定能找到一个唯一的x,使得p1 到px 一一对应着T 以及在拓扑序上比T 更靠后的强连通分量里节点的出度。显然,∑i=1xpi=(2x),因此∑i=1n[∑j=1ipj=(2i)] 不小于G 的强连通分量个数。
同时,∀x∈[1,n−1]∩Z,∑i=1xpi=(2x),px+1 到pn 对应的节点都向p1 到px 对应的节点连边,所以x 也唯一对应着链上相邻两个强连通分量的分界,因此∑i=1n[∑j=1ipj=(2i)] 不大于G 的强连通分量个数。考虑如果说一个强连通分量被分成了两半,如果后一半和后继的度数和等于(2n) 的话说明后一半的出度达到了最小值,就不能向前一半有连边,所以前一半和后一半就缺少了构成强连通分量的桥梁,所以通过反证法不成立。
综上,∑i=1n[∑j=1ipj=(2i)] 等于G 的强连通分量个数。在求出出度序列后,通过桶排,我们可以O(n) 求解一个n 阶竞赛图的强连通分量个数。
缩点后呈链状
这是一个很重要的性质,所有与环,SCC相关的问题都可以用这个性质解决。
即竞赛图强连通缩点后的 DAG 呈链状, 拓扑序小的点向所有拓扑序比它大的点连边,如下图。

证明考虑归纳,考虑一个一个加入连通块。目前假设有一条链,设插入的为块为x。若x 连向所有点,放头,若所有点连向x,放尾。否则找分界点插到中间即可(如果不能查到中间的话会成环)。

推论
- 若不存在位置i 满足如下条件,则为强连通图:
j=i∑nsi=(n−i+1)×(i−1)+(2n−i+1)
- 在同一个SCC中在比分序列上是一个区间,根据比分序列可以完成拓扑排序。
利用拓扑序小的点向所有拓扑序比它大的点连边,从后向前扫,用一个和上面判断 SCC本质相同(只是左右端点不同)的式子判定即可。
竞赛图存在一条哈密顿路径
证明,先缩点,如图构造:

竞赛图任意一个 SCC 存在一条哈密顿回路
考虑归纳, 逐点加入目前有一条链, 链上的每个强连通块都存在哈密顿回路,插入一个新点x,只需证明新图中的强连通块都存在哈密顿回路即可。如果不产生新连通块, 就是呈链中讨论的情况, 否则一定存在一条x 的出边在x 入边左边, 随便找一对
如果是连到不同连通块, 见左图.
如果是同一连通块, 必定存在符合环的走向的相邻的一入一出, 见右图.

强连通竞赛图也算整体一个 SCC,也就是强连通竞赛图有哈密顿回路。
竞赛图若有环一定存在三元环
考虑找到任意一个环,讨论一下顺时针还是逆时针。
例如顺时针,考虑环中的边,若为逆,直接结束有三元环。若为顺向的边可以看做环少了因这条边存在而绕过的点。
始终不出现逆向的边,最后一条边处构成三元环。
竞赛图的k>=3个点的SCC中一定存在[3,k]元环
归纳法证明即可,作者懒了不证明了。
3. 例题
3.1 计数
不知名题
求n 个点的强连通竞赛图个数。
考虑容斥,设f(i) 表示i 个点竞赛图个数,显然f(i)=22i(i−1),设g(i) 表示i 个点强连通竞赛图个数。
考虑转移,将图分成两部分,第一个部分是一个 SCC ,另外一个部分至少是一个 SCC ,枚举一个j 个点的 SCC 进行转移。
g(i)=f(i)−j=1∑i−1g(j)×(i−ji)×f(i−j)
m 及其小,而且值域很少,设一个图有n 个点,则边数上界就是30n,有2n(n−1)≤30n,解得n≤61。
用兰顿定理可以判断是否合法,设f(i,j,k) 表示能否用集合前i 个元素,构造出j 个点k 条边的图,转移方程有:
f(i,j,k)=f(i−1,j−1,k−am)orf(i,j−1,k−am)
让后考虑如何构造,发现可以发现一个竞赛图删除一个点以及它的所有边后仍然是一个竞赛图,那么避免冲突每次选择最小出度的点 更新边以及其他点的出度即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67
| #include<bits/stdc++.h> using namespace std; constexpr int MN=80,MM=2520; int m,n=1,a[MN],ret[MM],tmp[MM],ans[MN][MN]; bool f[MN][MN][MM];
bool cmp(int x,int y){ return ret[x]<ret[y]; }
void dfs(int x,int y,int z){ if(!x) return; ret[x]=a[y]; z-=a[y]; x--; if(f[x][y][z]) dfs(x,y,z); else dfs(x,y-1,z); }
void getans(){ for(int i=1;i<=n;i++) tmp[i]=i; for(int i=1;i<=n;i++){ sort(tmp+i,tmp+n+1,cmp); for(int j=i+1;j<=i+ret[tmp[i]];j++){ ans[tmp[i]][tmp[j]]=1; } for(int j=i+ret[tmp[i]]+1;j<=n;j++){ ans[tmp[j]][tmp[i]]=1; ret[tmp[j]]--; } } }
int main(){ cin>>m; for(int i=1;i<=m;i++){ cin>>a[i]; } sort(a+1,a+1+m); f[1][1][a[1]]=1; while(n<62&&(n<m||!f[n][m][n*(n-1)/2])){ n++; for(int i=1;i<=m;i++){ for(int j=(n-1)*(n-2)/2;j<=(n-1)*a[m];j++){ if(f[n-1][i][j]){ f[n][i][j+a[i]]=1; if(i+1<=m) f[n][i+1][j+a[i+1]]=1; } } } } if(n>61){ cout<<"=("; return 0; } dfs(n,m,n*(n-1)/2); getans(); cout<<n<<'\n'; for(int i=1;i<=n;i++){ for(int j=1;j<=n;j++){ cout<<ans[i][j]; } cout<<'\n'; } return 0; }
|
BZOJ 5219 最长路径 竞赛图组合计数
计数从 1 出发的最长简单路径经过点数恰好为i 的竞赛图个数。i∈[1,n],1≤n≤2000
竞赛图有个很好的性质:一定存在一条哈密尔顿路径。
考虑设f(i,j) 表示i 个点,最长路径为j 的竞赛图数量。
显然有f(i,i) 为强连通竞赛图,但是这里我们可以不用像上面不知名题求,就用容斥f(i,i)=22i(i−1)−j=1∑i−1f(i,j) 即可。
考虑f(i,1) 可以构造出所有 1 号点的边连向 1 号点即可,有f(i,1)=22(i−1)(i−2)。
那么剩下的怎么求,考虑将图划分为两个块,一个块是供 1 走的块,剩下的块不给 1 走,但是可能不存在不给 1 走的块。
那么有f(i,j)=f(j,j)×(j−1i−1)×22(i−j)(i−j−1),考虑j 个点连通块的连边方式,让后要选点,在把剩下的连边即可。
时间复杂度O(n2),但是可怕的是 BZOJ 挂了,有谁好心传个原题?
CF913F
令p=ba。
考虑倒推答案,设f(i) 表示i 个点的期望答案,g(i) 表示形成大小为i 的 SCC 概率,h(i,j) 表示i 个人打比赛,其中j 个人被(i−j) 个人打赢的概率。
最后一个 SCC 的大小,有:
f(i)=j=1∑ig(j)⋅h(i,j)⋅(f(j)+f(i−j)+(2j)+j(i−j))
但是f(i) 会转移到自己,要解方程,这里不过多叙述。
考虑gi 如何求,考虑只要图中没有点被其它点都吊打的情况它就是强联通图,那么转移:
g(i)=1−j=1∑i−1g(j)h(i,j)
考虑h,对于新加入一个点,如果在j 中那么输给i−j,反之要赢j 个点,那么有:
h(i,j)=(1−p)jh(i−1,j)+pi−jh(i−1,j−1)
时间O(n2)。
3.2 SCC 及其拓展
CF1268D
两个结论:
- 对于n≥4,n 阶强联通竞赛图具有 n−1 阶强联通子图。
- 对于 n≥7,n 阶竞赛图存在一种翻转方案使得只需要翻转一个结点就能让它强联通。
对于n≤6,暴力枚举翻转结点即可,对于n>6 考虑枚举每个位置翻转并检查就好了。我们可以用兰顿定理的推论(上面有讲)来快速判断是否强连通。
证明可以看其他题解:题解 CF1268D 【Invertation in Tournament】
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| #include<bits/stdc++.h> using namespace std; constexpr int MN=2520,MOD=998244353; int n,cf[MN],h[MN]; bool mp[MN][MN];
bool check(){ sort(cf+1,cf+1+n); for(int i=2;i<=n;i++) cf[i]+=cf[i-1]; for(int i=1;i<n;i++){ if(cf[i]==i*(i-1)/2) return 0; } return 1; }
int main(){ cin>>n; for(int i=1;i<=n;i++){ for(int j=1;j<=n;j++){ char c; cin>>c; mp[i][j]=c-'0'; h[i]+=mp[i][j]; } } for(int i=1;i<=n;i++) cf[i]=h[i]; if(check()){ cout<<"0 1"; return 0; } int ret=0; for(int i=1;i<=n;i++){ for(int j=1;j<=n;j++) cf[j]=h[j]; for(int j=1;j<=n;j++){ cf[i]-=mp[i][j]*2-1; cf[j]+=mp[i][j]*2-1; } if(check()) ret++; } if(ret){ cout<<1<<" "<<ret; return 0; } if(n==4) cout<<-1; if(n==6) cout<<"2 18"; return 0; }
|
定理上面都讲完了,先缩点,然后对于每个强连通分量,求哈密顿回路。然后就可以对任意一个强连通分量任意点进,任意点出了。
怎么构造可以去看其他题解的构造。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147
| #include<bits/stdc++.h> using namespace std; constexpr int MN=2e3+15; int n,tcnt,nxt[MN],a[MN][MN],in[MN],tpos[MN],pos[MN]; vector<int> adj[MN],G[MN],dcc[MN],ans[MN];
namespace Tarjan{ int dfn[MN],low[MN],s[MN],col[MN],ctot,top,dtot; bool vis[MN];
void tarjan(int u){ low[u]=dfn[u]=++dtot; s[++top]=u; vis[u]=1; for(auto v:adj[u]){ if(!dfn[v]){ tarjan(v); low[u]=min(low[u],low[v]); }else if(vis[v]){ low[u]=min(low[u],dfn[v]); } } if(low[u]==dfn[u]){ ctot++; int p; do{ p=s[top--]; col[p]=ctot; vis[p]=0; }while(p!=u); } } }using namespace Tarjan;
void toposort(){ queue<int> q; for(int i=1;i<=ctot;i++){ if(!in[i]) q.push(i); } while(!q.empty()){ int u=q.front(); q.pop(); tpos[++tcnt]=u; pos[u]=tcnt; for(auto v:G[u]){ in[v]--; if(!in[v]) q.push(v); } } }
void getham(int c){ if(dcc[c].size()==1) return; int s=dcc[c][0],t=s; for(int i=1;i<dcc[c].size();i++){ int x=dcc[c][i]; if(a[t][x]) nxt[t]=x,t=x; else if(a[x][s]) nxt[x]=s,s=x; else{ for(int j=s;j!=t;j=nxt[j]){ if(a[j][x]&&a[x][nxt[j]]){ nxt[x]=nxt[j]; nxt[j]=x; break; } } } } t=0; for(int i=nxt[s];i;i=nxt[i]){ if(t){ if(a[i][s]){ t=i; continue; } for(int j=s,k=nxt[s];j!=t;j=k,k=nxt[k]){ if(a[i][k]){ nxt[j]=nxt[t]; nxt[t]=s; s=k; t=i; break; } } }else if(a[i][s]) t=i; } nxt[t]=s; }
int main(){ cin>>n; for(int i=2;i<=n;i++){ for(int j=1;j<=i-1;j++){ int x; cin>>x; if(x){ adj[j].push_back(i); a[j][i]=1; }else{ adj[i].push_back(j); a[i][j]=1; } } } for(int i=1;i<=n;i++){ if(!dfn[i]) Tarjan::tarjan(i); } for(int i=1;i<=n;i++){ dcc[col[i]].push_back(i); } for(int u=1;u<=n;u++){ for(auto v:adj[u]){ if(col[u]!=col[v]){ G[col[u]].push_back(col[v]); in[col[v]]++; } } } toposort(); for(int i=1;i<=tcnt;i++){ getham(tpos[i]); } for(int i=1;i<=n;i++){ int lst=i,now=pos[col[i]]; while('QWQ'){ if(dcc[tpos[now]].size()==1){ ans[i].push_back(lst); if(now==tcnt) break; lst=dcc[tpos[++now]][0]; continue; } ans[i].push_back(lst); for(int j=nxt[lst];j!=lst;j=nxt[j]){ ans[i].push_back(j); } if(now==tcnt) break; lst=dcc[tpos[++now]][0]; } } for(int i=1;i<=n;i++){ cout<<ans[i].size()<<' '; for(auto p:ans[i]) cout<<p<<" "; cout<<'\n'; } return 0; }
|
4. 参考