WC2021 题解
T1 括号路径
有 \(n\) 个点 \(2m\) 条边的有向图,边有 \(k\) 种,每条边代表左括号或右括号,若 \((u,v)\) 有第 \(w\) 种左括号的边,则 \((v,u)\) 一定有同种的右括号的边。求有多少个点对 \((x,y)\) 满足 \(x\) 到 \(y\) 的路径是合法括号序列。
\(k\le n\le 3\times 10^5\),\(m\le 6\times 10^5\)。
并查集 + 线段树合并。
显然如果两个点有相同权值的出边只向同一个点,这两个点可以相互到达。我们可以把它们看作一个点,同样地这样做下去。那么用并查集维护合并的点,用动态开点线段树维护每个点的入边,合并点同时合并线段树。
每合并两个点同时也会合并很多点,实测每次直接合并线段树会爆栈 MLE,用一个队列把所有合并操作存下,像 bfs 一样把该合并的合并就好了(需要额外一个并查集来维护线段树的合并情况)。最后答案就是每个集合选两个点的方案数之和。复杂的 \(\mathcal O(n\log n)\)。
code
typedef long long LL;
const int N = 3e5 + 5;
const int NL = 5e6 + 5;
struct UnionSet {
int fa[N], size[N];
inline void init(int n) {
for (int i = 1; i <= n; ++i) fa[i] = i, size[i] = 1;
}
int find(int x) {
return fa[x] == x ? x : fa[x] = find(fa[x]);
}
inline void link(int x, int y) {
x = find(x), y = find(y);
if (x != y) fa[y] = x, size[x] += size[y];
}
} S0, S1;
queue<pair<int, int> > q;
struct SegmentTree {
int tot;
int ls[NL], rs[NL], id[NL], rt[N];
void insert(int &suc, int l, int r, int p, int x) {
if (!suc) suc = ++tot;
if (l == r) {
if (!id[suc]) return id[suc] = x, void();
S0.link(id[suc], x);
q.push(make_pair(id[suc], x));
return;
}
int mid = (l + r) >> 1;
if (p <= mid) insert(ls[suc], l, mid, p, x);
else insert(rs[suc], mid+1, r, p, x);
}
void merge(int &u, int v, int l, int r) {
if (!u || !v) return u |= v, void();
if (l == r) {
if (!id[u] || !id[v]) return id[u] |= id[v], void();
S0.link(id[u], id[v]);
q.push(make_pair(id[u], id[v]));
return;
}
int mid = (l + r) >> 1;
merge(ls[u], ls[v], l, mid);
merge(rs[u], rs[v], mid+1, r);
}
} T;
int n, m, k;
LL ans;
inline void main() {
cin >> n >> m >> k;
S0.init(n), S1.init(n);
for (int i = 1; i <= m; ++i) {
int u, v, w;
cin >> u >> v >> w;
T.insert(T.rt[v], 1, k, w, u);
}
while (!q.empty()) {
pair<int, int> u = q.front();
q.pop();
int x = S1.find(u.first), y = S1.find(u.second);
if (x == y) continue;
T.merge(T.rt[x], T.rt[y], 1, k);
S1.link(x, y);
}
for (int i = 1; i <= n; ++i)
if (i == S1.find(i))
ans += (LL)S1.size[i] * (S1.size[i] - 1) / 2;
cout << ans << '\n';
}
T2 表达式求值
给出表达式串 \(E\)(包括
(、))和 \(m\) 个长为 \(n\) 到序列,运算符>代表两个序列按位取 \(\max\),运算符<代表两个序列按位取 \(\min\),?代表不确定。若有 \(t\) 个?,求出 \(2^t\) 个可能的 \(E\) 表达式串的结果所有数的和。\(m\le 10\),\(n,|E|\le 5\times 10^4\)。
树形 DP + 容斥。
首先发现这 \(n\) 位互不影响,分开考虑,然后把表达式树建出来。问题转化为一个序列 \(a_1\dots a_m\) 在表达式树上 \(2^t\) 个运算结果之和。
\(m\) 范围很小,所以我们要想办法求出每个值作为结果的方案数,再乘上该值就好。考虑在表达式树上做一个树形 DP,发现 \(\min/\max\) 运算结果只和两者相对大小有关,同时要减小状态量,那么对于每一个 \(a_i\),设 \(f(S,u,0/1)\) 表示当前比 \(a_i\) 小的元素集合为 \(S\),以 \(u\) 为根的子树得到结果小于 / 大于等于 \(a_i\) 的方案数。然后做一个容斥。
转移就很容易:当 \(u\) 代表 < 时,
> 同理,? 就是两种情况都算。我们把 \(m\) 个元素的集合状压,并算出所有 \(S\) 的答案,后面查表即可。这里复杂度 \(\mathcal O(2^m|E|)\)。
然后我们得到 \(g(S_i)=f(S_i,root,1)\) 代表比 \(a_i\) 小的元素集合为 \(S\) 时答案大于等于 \(a_i\) 的方案数。考虑容斥得到最终答案,我们把 \(\{a\}\) 从小到大排序得到下标排列 \(\{b\}\),答案为:
这样总复杂度就是 \(\mathcal O(2^m|E|+nm\log m)\)。
code
typedef long long LL;
const int N = 5e4 + 5;
const int M = (1 << 10) + 5;
const int P = 1e9 + 7;
int n, m, slen, now, ans;
int a[11][N], b[11], g[M];
char str[N];
struct ExprTree {
int tot, root;
int ls[N], rs[N], val[N], stk[N], ptr[N], f[N][2];
inline void init() {
for (int i = 1; i <= slen; ++i) ptr[i] = i;
int top = 0;
for (int i = 1; i <= slen; ++i) {
if (str[i] == '(') stk[++top] = i;
if (str[i] == ')') {
int x = stk[top--];
ptr[x] = i, ptr[i] = x;
}
}
root = build(1, slen);
}
int build(int l, int r) {
if (l < r && ptr[r] == l) return build(l + 1, r - 1);
int suc = ++tot;
if (l == r) return val[suc] = str[l] - '0', suc;
val[suc] = str[ptr[r] - 1];
ls[suc] = build(l, ptr[r] - 2), rs[suc] = build(ptr[r], r);
return suc;
}
inline int solve(int s) {
dp(root, s);
return f[root][1];
}
void dp(int u, int s) {
f[u][0] = f[u][1] = 0;
if (!ls[u]) {
f[u][((s >> val[u]) & 1) ^ 1] = 1;
return;
}
dp(ls[u], s), dp(rs[u], s);
if (val[u] != '>') {
for (int i = 0; i < 2; ++i)
for (int j = 0; j < 2; ++j) {
int k = min(i, j);
f[u][k] = (f[u][k] + (LL)f[ls[u]][i] * f[rs[u]][j]) % P;
}
}
if (val[u] != '<') {
for (int i = 0; i < 2; ++i)
for (int j = 0; j < 2; ++j) {
int k = max(i, j);
f[u][k] = (f[u][k] + (LL)f[ls[u]][i] * f[rs[u]][j]) % P;
}
}
}
} T;
bool cmp(const int &x, const int &y) {
return a[x][now] < a[y][now];
}
inline void main() {
cin >> n >> m;
for (int i = 1; i <= m; ++i)
for (int j = 1; j <= n; ++j)
cin >> a[i][j];
cin >> (str + 1);
slen = strlen(str + 1);
T.init();
int maxs = 1 << m;
for (int s = 0; s < maxs; ++s)
g[s] = T.solve(s);
for (int i = 1; i <= m; ++i) b[i] = i;
for (int i = 1; i <= n; ++i) {
now = i;
sort(b+1, b+m+1, cmp);
int s = 0;
for (int j = 1; j <= m; ++j) {
if (j > 1) s |= 1 << (b[j - 1] - 1);
ans = (ans + (LL)g[s] * (a[b[j]][i] - a[b[j - 1]][i])) % P;
}
}
cout << ans << '\n';
}
T3 斐波那契
定义 \(F_0=a\),\(F_1=b\),\(F_i=(F_{i-1}+F_{i-2})\bmod m\quad(i\ge 2)\)。
\(n\) 次询问给出 \(a,b\),求最小的 \(p\) 满足 \(F_p=0\)。
\(n,m\le 10^5\)。
数论。
带着 \(a,b\) 往后推几项,可以发现 \(a,b\) 的系数就是斐波那契数列的相邻两项。设斐波那契数 \(\{f\}\),令 \(f_0=0\),\(f_1=1\)。那么 \(F_n=(f_{n-1}a+f_nb)\bmod m\)。
我们知道一个结论:\(\bmod m\) 意义下的斐波那契数列是有循环节的,且循环节长度不超过 \(6m\)。
令 \(F_n=0\),我们显然可以得到 \(f_{n-1}a+f_nb\equiv 0\pmod m\),再化一下就是 \(\displaystyle -\frac{a}{b}\equiv\frac{f_n}{f_{n-1}}\pmod m\)。这样我们只要处理一个循环节的斐波那契数列后每次查表即可。(\(40\) 分)
但是 \(m\) 是合数,不一定能求逆元。对于
约去 \(\gcd(a,-b,m)\) 得到
但是此时 \(m'\) 对于四者依然不一定互质。然后由于 \(\gcd(f_n,f_{n-1})=\gcd(n,n-1)=1\),设
整个式子再都约去 \(pq\) 就满足模数与每个数互质。
以 \(p,q\) 作为媒介,我们能够得到 \(a',b'\) 与 \(f_n,f_{n-1}\) 的对应关系。考虑对于所有的 \(m'|m\),处理出所有 \(\frac{f_n}{f_{n-1}}\) 以及对应 \(p,q\),组成三元组作为 map 的键值。复杂度 \(\mathcal O(\sigma(m))\),\(\sigma(m)\) 代表 \(m\) 的约数和。
加上每次查表,总复杂度 \(\mathcal O(\sigma(m)+n\log m)\)。
需要注意 \(a=0\) 或 \(b=0\) 的情况。
code
typedef long long LL;
const int N = 1e5 + 5;
struct Tuple {
int x, y, z;
Tuple(int _x = 0, int _y = 0, int _z = 0): x(_x), y(_y), z(_z) {}
friend const bool operator <(const Tuple &a, const Tuple &b) {
if (a.x ^ b.x) return a.x < b.x;
if (a.y ^ b.y) return a.y < b.y;
return a.z < b.z;
}
};
int gcd(const int &x, const int &y) {
return !y ? x : gcd(y, x % y);
}
void exgcd(int a, int b, int &x, int &y) {
if (!b) {
x = 1, y = 0;
return;
}
exgcd(b, a % b, y, x);
y -= (a / b) * x;
}
inline int inv(int a, int p) {
int x, y;
exgcd(a, p, x, y);
return (x % p + p) % p;
}
int n, m;
map<Tuple, int> mp[N];
inline void main() {
cin >> n >> m;
for (int mod = 2; mod <= m; ++mod) {
if (m % mod) continue;
int x = 1, y = 0;
for (int i = 0; ; ++i) {
if (x && y) {
int p = gcd(y, mod), q = gcd(x, mod), m1 = mod / p / q;
int yy = y / p, xx = x / q;
Tuple t(p, q, int((LL)yy * inv(xx, m1) % m1));
if (!mp[mod][t]) mp[mod][t] = i;
}
x = (x + y) % mod, swap(x, y);
if (x == 1 && y == 0) break;
}
}
while (n--) {
int a, b;
cin >> a >> b;
b = (m - b) % m;
if (!a) {
cout << 0 << '\n';
continue;
}
if (!b) {
cout << 1 << '\n';
continue;
}
int d = gcd(gcd(a, b), m), mod = m / d;
a /= d, b /= d;
int p = gcd(a, mod), q = gcd(b, mod), m1 = mod / p / q;
a /= p, b /= q; // bug
Tuple t(p, q, int((LL)a * inv(b, m1) % m1));
if (mp[mod].count(t)) cout << mp[mod][t] << '\n';
else cout << -1 << '\n';
}
}

浙公网安备 33010602011771号