NOI2021 Day1
轻重边
把询问和修改都转到点上考虑。
相当于给某些路径上的点都染上一种未出现过的颜色,然后查询某些路径上颜色相同的相邻点对数。
注意初始时所有点的颜色应该互不相同
树剖 + 线段树就做完了。需要特别注意的是树剖跳链时也会产生贡献。
时间复杂度 \(\mathcal O(n \log^2 n)\)。
代码
#include <bits/stdc++.h>
using namespace std;
using uint = unsigned int;
using ll = long long;
using ull = unsigned long long;
using lll = __int128;
using ld = long double;
template<typename tp> inline void chkmax(tp &x, tp y) {x = max(x, y);}
template<typename tp> inline void chkmin(tp &x, tp y) {x = min(x, y);}
constexpr int N = 1e5 + 10;
int n, m, cnt, fa[N], dep[N], sz[N], son[N], dfn[N], top[N];
vector<int> G[N];
namespace SGT {
#define ls o << 1
#define rs o << 1 | 1
constexpr int TN = N << 2;
int lc[TN], rc[TN], cnt[TN], lz[TN];
inline void pu(int o) {
lc[o] = lc[ls], rc[o] = rc[rs];
cnt[o] = cnt[ls] + cnt[rs] + (rc[ls] == lc[rs]);
}
inline void pd(int o, int l, int r) {
if (lz[o]) {
int mid = (l + r) >> 1;
lc[ls] = rc[ls] = lz[ls] = lc[rs] = rc[rs] = lz[rs] = lz[o];
cnt[ls] = mid - l, cnt[rs] = r - mid - 1;
lz[o] = 0;
}
}
void build(int o, int l, int r) {
lz[o] = cnt[o] = 0;
if (l == r) {lc[o] = rc[o] = l; return;}
int mid = (l + r) >> 1;
build(ls, l, mid), build(rs, mid + 1, r);
pu(o);
}
void upd(int o, int l, int r, int x, int y, int c) {
if (x <= l && r <= y) {lc[o] = rc[o] = lz[o] = c, cnt[o] = r - l; return;}
pd(o, l, r); int mid = (l + r) >> 1;
if (x <= mid) upd(ls, l, mid, x, y, c);
if (y > mid) upd(rs, mid + 1, r, x, y, c);
pu(o);
}
int qry(int o, int l, int r, int x, int y) {
if (x <= l && r <= y) return cnt[o];
pd(o, l, r); int mid = (l + r) >> 1;
if (y <= mid) return qry(ls, l, mid, x, y);
if (x > mid) return qry(rs, mid + 1, r, x, y);
return qry(ls, l, mid, x, y) + qry(rs, mid + 1, r, x, y) + (rc[ls] == lc[rs]);
}
int col(int o, int l, int r, int x) {
if (l == r) return lc[o];
pd(o, l, r); int mid = (l + r) >> 1;
return x <= mid ? col(ls, l, mid, x) : col(rs, mid + 1, r, x);
}
}
void dfs1(int u) {
sz[u] = 1, son[u] = 0;
for (int v : G[u]) if (v != fa[u]) {
dep[v] = dep[fa[v] = u] + 1, dfs1(v);
sz[u] += sz[v];
if (sz[v] > sz[son[u]]) son[u] = v;
}
}
void dfs2(int u, int tp) {
dfn[u] = ++cnt, top[u] = tp;
if (!son[u]) return;
dfs2(son[u], tp);
for (int v : G[u]) if (v != fa[u] && v != son[u]) dfs2(v, v);
}
void upd(int x, int y, int c) {
while (top[x] != top[y]) {
if (dep[top[x]] < dep[top[y]]) swap(x, y);
SGT::upd(1, 1, n, dfn[top[x]], dfn[x], c);
x = fa[top[x]];
}
if (dfn[x] > dfn[y]) swap(x, y);
SGT::upd(1, 1, n, dfn[x], dfn[y], c);
}
int qry(int x, int y) {
int res = 0;
while (top[x] != top[y]) {
if (dep[top[x]] < dep[top[y]]) swap(x, y);
res += SGT::qry(1, 1, n, dfn[top[x]], dfn[x]) + (SGT::col(1, 1, n, dfn[top[x]]) == SGT::col(1, 1, n, dfn[fa[top[x]]]));
x = fa[top[x]];
}
if (dfn[x] > dfn[y]) swap(x, y);
return res + SGT::qry(1, 1, n, dfn[x], dfn[y]);
}
void solve() {
cnt = 0;
for (int i = 1; i <= n; i++) G[i].clear();
cin >> n >> m;
for (int i = 1, u, v; i < n; i++) cin >> u >> v, G[u].emplace_back(v), G[v].emplace_back(u);
dfs1(dep[1] = 1), dfs2(1, 1);
SGT::build(1, 1, n);
for (int i = 1, op, x, y; i <= m; i++) {
cin >> op >> x >> y;
if (op == 1) upd(x, y, n + i);
else cout << qry(x, y) << '\n';
}
}
int main() {
ios_base::sync_with_stdio(0); cin.tie(nullptr), cout.tie(nullptr);
int t; cin >> t;
while (t--) solve();
return 0;
}
路径交点
两条路径间交点的奇偶性近由它们起点和终点的相对位置决定,更具体地,记他们的起点分别为 \(S_1, S_2\),终点分别为 \(T_1, T_2\),则 \(S_1 \rightsquigarrow T_1\) 和 \(S_2 \rightsquigarrow T_2\) 有奇数个交点当且仅当 \((S_1 - S_2) \times (T_1 - T_2) < 0\)。
如果我们按起点编号从大到小考虑每条路径的终点,记终点的排列为 \(\{p_n\}\),则 \(\{p_n\}\) 中形成逆序对的 \((i, j)\) 有奇数个交点,其余有偶数个交点,也就是说 路径方案中交点个数的奇偶性与 \(\{p_n\}\) 中逆序对数的奇偶性相同。
所以一个猜想就是直接把邻接矩阵乘起来求行列式,时间复杂度 \(\mathcal O(kn^3)\)。
然后你 AC 了。
再然后你开始疑惑:
现在小 L 要从这个图中选出 \(n_1\) 条路径,每条路径以第 \(1\) 层顶点为起点,第 \(k\) 层顶点为终点,并要求图中的每个顶点至多出现在一条路径中。
“我没考虑这个要求啊?”
不考虑就对了。
如果某个方案里存在这样两条路径: \(S_1 \rightsquigarrow M \rightsquigarrow T_1\) 和 \(S_2 \rightsquigarrow M \rightsquigarrow T_2\),一定存在且仅存在一个除了这两条路径之外和它完全相同的方案,满足这两条路径是 \(S_1 \rightsquigarrow M \rightsquigarrow T_2\) 和 \(S_2 \rightsquigarrow M \rightsquigarrow T_1\),又因为排列中交换两个元素的位置后逆序对数的奇偶性一定会发生变化,所以这两种情况抵消了。
代码
#include <bits/stdc++.h>
using namespace std;
using uint = unsigned int;
using ll = long long;
using ull = unsigned long long;
using lll = __int128;
using ld = long double;
template<typename tp> inline void chkmax(tp &x, tp y) {x = max(x, y);}
template<typename tp> inline void chkmin(tp &x, tp y) {x = min(x, y);}
constexpr int N = 201, MOD = 998244353;
int n[N], m[N];
ll inv(ll base, int e = MOD - 2) {
ll res = 1;
while (e) {
if (e & 1) res = res * base % MOD;
base = base * base % MOD;
e >>= 1;
}
return res;
}
struct Matrix {
ll a[N][N];
Matrix() {memset(a, 0, sizeof(a));}
Matrix mul(int n2, int n3, Matrix rhs) { // (n1, n2) * (n2, n3) -> (n1, n3)
Matrix res;
for (int i = 1; i <= n[1]; i++) {
for (int j = 1; j <= n3; j++) {
for (int k = 1; k <= n2; k++) {
res.a[i][j] = (res.a[i][j] + a[i][k] * rhs.a[k][j]) % MOD;
}
}
}
return res;
}
ll det(int n) {
ll res = 1, f = 1;
for (int i = 1; i <= n; i++) {
int p = i;
while (p <= n && !a[p][i]) p++;
if (p > n) return 0;
if (p != i) swap(a[p], a[i]), f = -f;
res = res * a[i][i] % MOD;
ll t = inv(a[i][i]);
for (int j = i; j <= n; j++) a[i][j] = a[i][j] * t % MOD;
for (int j = i + 1; j <= n; j++) {
ll coef = MOD - a[j][i];
for (int k = i; k <= n; k++) a[j][k] = (a[j][k] + coef * a[i][k]) % MOD;
}
}
return (res * f + MOD) % MOD;
}
};
void solve() {
int k; cin >> k;
for (int i = 1; i <= k; i++) cin >> n[i];
for (int i = 1; i < k; i++) cin >> m[i];
Matrix E;
for (int i = 1; i < k; i++) {
Matrix now;
for (int u, v; m[i]--;) cin >> u >> v, now.a[u][v] = 1;
E = i > 1 ? E.mul(n[i], n[i + 1], now) : now;
}
cout << E.det(n[1]) << '\n';
}
int main() {
ios_base::sync_with_stdio(0); cin.tie(nullptr), cout.tie(nullptr);
int t; cin >> t;
while (t--) solve();
return 0;
}
庆典
先 tarjan 缩成一个 DAG。
考虑给定的条件:
-
若 \(x \Rightarrow z\) 且 \(y \Rightarrow z\),则 \(x \Rightarrow y\) 或 \(y \Rightarrow x\)。
-
把有向边视作无向边后,图强连通。
因为是 DAG,所以我们一定可以找到一个入度为 \(0\) 的点,记作 \(x_0\),那么对于剩下的所有点 \(z\),必然有 \(x_0 \Rightarrow z\) 或 \(x_0 \Rightarrow y\) 且 \(y \Rightarrow z\),对于后者显然不应该存在 \(z \Rightarrow x_0\),所以一定有 \(x_0 \Rightarrow z\),也即图弱连通。
对于每一个终点,我们只保留 起点拓扑序最大 的边,图的连通性仍不变,但变成了更简单的以 \(x_0\) 为根的外向树。
\(k = 0\) 时就是简单地查询 \(t\) 是否在 \(s\) 子树内,若是则查询 \(s\) 到 \(t\) 路径上的点权和。
\(k = 1,2\) 的情况都可以通过树剖后的巨大分讨解决,但有一种更巧妙的办法:虚树。
以 \(s\),\(t\) 及新增路径的起点和终点为关键点建立虚树,在虚树上两次 bfs 分别求 \(S = \{x | s \Rightarrow x\}\) 和 \(T = \{x | x \in S, x \Rightarrow t\}\),\(|T|\) 就是答案。
时间复杂度 \(\mathcal O(n \log n + qk (\log k + \log n))\)。
时间卡得还是有点死(反正在原题的时间限制(\(1 \text s\))下我只能写到 \(44\) 分)。
瓶颈在建虚树的多次 \(\mathcal O(\log n)\) 求 LCA,改成 \(\mathcal O(n \log n) - \mathcal O(1)\) 的 dfs 序 \(\text{LCA}\) 后可以把时间复杂砍到 \(\mathcal O(n \log n + qk \log k)\),然后就能过了。
不同于树剖,这个做法可以拓展到 \(\sum k \le 6 \times 10^5\)。
卡常题,效果比较显著的几个点是:
- 快读(快写可有可无,输出规模并不算特别大)。
- 极限数据下图比较稀疏,链式前向星(邻接表)存图明显快于
vector
。
代码
#include <bits/stdc++.h>
using namespace std;
using uint = unsigned int;
using ll = long long;
using ull = unsigned long long;
using lll = __int128;
using ld = long double;
template<typename tp> inline void chkmax(tp &x, tp y) {x = max(x, y);}
template<typename tp> inline void chkmin(tp &x, tp y) {x = min(x, y);}
template<typename _T> inline void read(_T &_x) {
_x = 0;
_T _f = 1;
char _ch = getchar();
while (_ch < '0' || '9' < _ch) {
if (_ch == '-') _f = -1;
_ch = getchar();
}
while ('0' <= _ch && _ch <= '9') {
_x = (_x << 3) + (_x << 1) + (_ch & 15);
_ch = getchar();
}
_x *= _f;
}
template<typename _T> void write(_T _x) {
if (_x < 0) {
putchar('-');
_x = -_x;
}
if (_x > 9) write(_x / 10);
putchar('0' + _x % 10);
}
constexpr int N = 3e5 + 10, K = 20;
int n, m, qn, k, p, rt, w[N];
struct Graph1 {
int tot, head[N];
struct Edge {int to, nxt;} e[N << 1];
inline void add(int u, int v) {e[++tot] = Edge{v, head[u]}, head[u] = tot;}
} G0, G1, G;
namespace SCC {
int cnt, dfn[N], low[N], top, stk[N], bel[N]; bool ins[N];
void tarjan(int u) {
dfn[u] = low[u] = ++cnt, ins[stk[++top] = u] = 1;
for (int i = G0.head[u], v; v = G0.e[i].to, i; i = G0.e[i].nxt) {
if (!dfn[v]) tarjan(v), low[u] = min(low[u], low[v]);
else if (ins[v]) low[u] = min(low[u], dfn[v]);
}
if (dfn[u] == low[u]) {
p++; int x;
do ins[x = stk[top--]] = 0, w[p]++, bel[x] = p;
while (x != u);
}
}
int ind[N];
queue<int> q;
void work() {
for (int i = 1; i <= n; i++) if (!dfn[i]) tarjan(i);
for (int u = 1; u <= n; u++) for (int i = G0.head[u], v; v = G0.e[i].to, i; i = G0.e[i].nxt) if (bel[u] != bel[v]) G1.add(bel[u], bel[v]), ind[bel[v]]++;
for (int i = 1; i <= p; i++) if (!ind[i]) q.emplace(rt = i);
while (!q.empty()) {
int u = q.front(); q.pop();
for (int i = G1.head[u], v; v = G1.e[i].to, i; i = G1.e[i].nxt) if (!(--ind[v])) {
G.add(u, v);
q.emplace(v);
}
}
}
}
int cnt, dfn[N], s[N], dep[N], fa[N], lg[N], st[K][N];
void predfs(int u) {
dfn[u] = ++cnt, st[0][cnt] = fa[u], s[u] = w[u] + s[fa[u]];
for (int i = G.head[u], v; v = G.e[i].to, i; i = G.e[i].nxt) if (v != fa[u]) {
dep[v] = dep[fa[v] = u] + 1, predfs(v);
}
}
inline int mindep(int x, int y) {return dep[x] < dep[y] ? x : y;}
int LCA(int u, int v) {
if (u == v) return u;
u = dfn[u], v = dfn[v];
if (u > v) swap(u, v);
return mindep(st[lg[v - u]][u + 1], st[lg[v - u]][v - (1 << lg[v - u]) + 1]);
}
int nkey, key[N];
unordered_map<int, int> pid;
inline int id(int x) {return pid[x] ? pid[x] : pid[x] = ++n;};
int val[N];
struct Graph2 {
int tot, head[K << 1];
struct Edge {int to, nxt, w;} e[K * K];
inline void clear() {tot = 0; memset(head, 0, sizeof(head));}
inline void add(int u, int v, int w) {e[++tot] = {v, head[u], w}, head[u] = tot;}
} H, iH;
inline void add(int u, int v, int w) {H.add(id(u), id(v), w), iH.add(id(v), id(u), w);}
inline bool cmp(const int &i, const int &j) {return dfn[i] < dfn[j];}
queue<int> q;
bool S[K], vis[K], vit[K];
int main() {
read(n), read(m), read(qn), read(k);
for (int i = 1, u, v; i <= m; i++) read(u), read(v), G0.add(u, v);
SCC::work();
dep[rt] = 1, predfs(rt);
for (int i = 2; i <= n; i++) lg[i] = lg[i >> 1] + 1;
for (int i = 1; (1 << i) <= n; i++) for (int j = 1; j + (1 << i) - 1 <= n; j++) st[i][j] = mindep(st[i - 1][j], st[i - 1][j + (1 << (i - 1))]);
while (qn--) {
// clear
pid.clear(), H.clear(), iH.clear();
n = 0, nkey = 2;
// work
int x, y; read(x), read(y), x = SCC::bel[x], y = SCC::bel[y];
key[1] = x, key[2] = y;
for (int i = 1, u, v; i <= k; i++) {
read(u), read(v), u = SCC::bel[u], v = SCC::bel[v];
if (u != v) {
key[++nkey] = u, key[++nkey] = v;
add(u, v, 0);
}
}
sort(key + 1, key + nkey + 1, cmp), nkey = unique(key + 1, key + nkey + 1) - key - 1;
int tmp = nkey;
for (int i = 2; i <= tmp; i++) key[++nkey] = LCA(key[i - 1], key[i]);
sort(key + 1, key + nkey + 1, cmp), nkey = unique(key + 1, key + nkey + 1) - key - 1;
val[id(key[1])] = w[key[1]];
for (int i = 2, lca; i <= nkey; i++) {
lca = LCA(key[i - 1], key[i]);
val[id(key[i])] = w[key[i]], val[id(lca)] = w[lca];
add(lca, key[i], s[fa[key[i]]] - s[lca]);
}
memset(vis, 0, sizeof(vis)), memset(S, 0, sizeof(S));
q.push(id(x));
while (!q.empty()) {
int u = q.front(); q.pop();
S[u] = 1;
for (int i = H.head[u], v; v = H.e[i].to, i; i = H.e[i].nxt) if (!vis[i]) {
vis[i] = 1, q.emplace(v);
}
}
int ans = 0;
memset(vit, 0, sizeof(vit));
q.push(id(y));
while (!q.empty()) {
int u = q.front(); q.pop();
if (S[u]) ans += val[u], S[u] = 0;
for (int i = iH.head[u], v; v = iH.e[i].to, i; i = iH.e[i].nxt) if (!vit[i]) {
vit[i] = 1;
if (vis[i]) ans += iH.e[i].w, vis[i] = 0;
q.emplace(v);
}
}
write(ans), putchar('\n');
}
return 0;
}