竞赛图小记
基本性质
首先竞赛图就是一个
竞赛图看起来有
这个链可以说是广义的链,因为除去主链之外的边都是不会改变连通性的边:
也就是说,缩点之后假如一条主链为
为什么会是这样的一个结构呢?实际上很好理解,首先缩点之后的有向图就是一个 DAG。因为任意两点之间都有恰好一条边,而对于一个新点
仔细瞪一瞪 这个广义的链形态!
CF1498E. Two Houses (*2200)
竞赛图,给出了点的入度。问的是连通性问题,所以容易想到考虑缩点之后来做。实际上竞赛图的性质基本上都是在缩点之后体现出来的,所以碰到的时候考虑缩点一下可能会更容易发现大量性质!
缩点之后就得到了上面那样的一条链,我们考虑,因为询问出了一个
比如图中的
也就是说,如果 Yes
,否则回答 ! 0 0
。
复杂度
tp HARUTO() {
tp n; cin >> n;
vector<tp> d(n + 1, 0);
for (tp i = 1; i <= n; ++i) cin >> d[i], d[i] = n - 1 - d[i];
vector<tuple<tp, tp, tp>> v;
for (tp i = 1; i <= n; ++i) {
for (tp j = i + 1; j <= n; ++j) {
if (d[i] <= d[j]) v.emplace_back(d[j] - d[i], i, j);
else v.emplace_back(d[i] - d[j], j, i);
}
}
sort(FULL(v), greater<>());
for (auto [p, x, y] : v) {
cout << "? " << x << ' ' << y << endl;
string c; cin >> c;
if (c[0] == 'Y') return cout << "! " << x << ' ' << y << endl, 0;
}
cout << "! 0 0" << endl;
return 0;
}
CF804F. Fake bullions (*3400)
喜欢我们的难度 delta 吗?!
可以说是强行二合一,所以我们分两个部分来做:
求一个点的假金条数
我们考虑现在有两个点,满足
于是我们可以维护一个数组
如果直接暴力更新,我们发现因为图不是 DAG,会产生问题。于是我们考虑先缩点。一个强连通分量内,我们可以直接把所有点弄到
缩点之后的竞赛图是一条链,我们只要保留一条贯穿所有连通块的从一个点开始的链就可以更新了,具体我们从链的起点开始,对于 bitset
维护,这里复杂度正确只是因为每个点只要用于更新,被更新各 bitset
,不管了,不在本文讨论范围内!
现在我们就能够算出一个点上的真假金条分别的数量了。
统计最终的方案数
感觉这个部分有点小难想啊,想要不容斥并且统计得不重不漏还是需要一点技巧的。
我们容易想到,记
现在是非严格前
我们钦定这
我们可以把除去
对于一个点
- 如果
mn_j>mx_i ,就说明是必选,归入c_1 。 - 否则如果
mx_j>mx_i 或者mx_j=mx_i,j<i ,就说明是可以选到n_2 的,归入c_2 。
那么我们考虑枚举这
复杂度
constexpr tp N = 5e3 + 5, M = 2e6 + 5;
array<tp, N> fac, ifc;
void QWQ() {
//freopen(".in", "r", stdin), freopen(".out", "w", stdout);
ios::sync_with_stdio(false), cin.tie(nullptr);
tp n = 5e3;
fac[0] = 1;
for (tp i = 1; i <= n; ++i) fac[i] = 1ll * fac[i - 1] * i % mod;
ifc[n] = pow(fac[n], mod - 2);
for (tp i = n - 1; ~i; --i) ifc[i] = 1ll * ifc[i + 1] * (i + 1) % mod;
return ;
}
tp bn(tp n, tp m) {
if (n < 0 || m < 0 || n < m) return 0;
return 1ll * fac[n] * ifc[m] % mod * ifc[n - m] % mod;
}
tp n, n1, n2;
array<vector<tp>, N> e;
array<tp, N> s;
array<vector<bool>, N> a;
array<bool, M> b;
tp tot, cnt;
array<tp, N> dfn, low, bel, g, mn, mx;
array<bool, N> ins;
stack<tp> stk;
array<vector<tp>, N> scc;
void upd1(tp x, tp d) {
for (auto u : scc[x])
for (tp i = 0; i < s[u]; ++i) b[i % d] |= a[u][i];
return ;
}
void upd2(tp x, tp d) {
for (auto u : scc[x])
for (tp i = 0; i < s[u]; ++i)
if (b[i % d] && !a[u][i]) a[u][i] = 1, mx[u]++;
return ;
}
void upd3(tp d) {
for (tp i = 0; i < d; ++i) b[i] = 0;
return ;
}
void tar(tp u) {
dfn[u] = low[u] = ++tot;
stk.emplace(u), ins[u] = 1;
for (auto v : e[u]) {
if (!dfn[v])
tar(v), ckmin(low[u], low[v]);
if (ins[v]) ckmin(low[u], dfn[v]);
}
if (dfn[u] == low[u]) {
++cnt;
while (1) {
auto v = stk.top(); stk.pop(), ins[v] = 0;
scc[cnt].emplace_back(v), bel[v] = cnt, g[cnt] = __gcd(g[cnt], s[v]);
if (v == u) break;
}
upd1(cnt, g[cnt]), upd2(cnt, g[cnt]), upd3(g[cnt]);
}
return ;
}
array<tp, N> out;
tp HARUTO() {
cin >> n >> n1 >> n2;
for (tp i = 1; i <= n; ++i) {
for (tp j = 1; j <= n; ++j) {
char c; cin >> c;
if (c == '1') e[i].emplace_back(j);
}
}
for (tp i = 1; i <= n; ++i) {
cin >> s[i], a[i].assign(s[i], 0);
char c;
for (tp j = 0; j < s[i]; ++j)
cin >> c, a[i][j] = c - '0', mx[i] += a[i][j];
mn[i] = mx[i];
}
for (tp i = 1; i <= n; ++i)
if (!dfn[i]) tar(i);
for (tp u = 1; u <= n; ++u)
for (auto v : e[u])
if (bel[u] != bel[v]) out[bel[u]]++;
vector<tp> ord(tot + 1, 0);
iota(ALL(ord, 1, tot), 1), sort(ALL(ord, 1, tot), [&](tp x, tp y) { return out[x] > out[y]; });
for (tp i = 1; i < tot; ++i) {
tp u = ord[i], v = ord[i + 1], d = __gcd(g[u], g[v]);
upd1(u, d), upd2(v, d), upd3(d);
}
tp ans = 0;
for (tp i = 1; i <= n; ++i) {
tp c1 = 0, c2 = 0;
for (tp j = 1; j <= n; ++j) {
if (mn[j] > mx[i]) c1++;
else if (mx[j] > mx[i] || (mx[j] == mx[i] && j < i)) c2++;
}
for (tp j = 0; j + c1 < n1; ++j)
add(ans, 1ll * bn(c2, j) * bn(c1, n2 - j - 1) % mod);
}
cout << ans << "\n";
return 0;
}
int main() {
QWQ();
tp t = 1;
while (t--) HARUTO();
return 0;
}
小结
相信大家看到这里,都能明白一件事,竞赛图的题目的难点其实不在于竞赛图的部分,而是在于利用了竞赛图缩点后的性质,在连通性问题上面之后的处理。而竞赛图的性质也可以说是和连通性问题捆绑了。
希望大家学会了这个 trick。