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\) 代表 < 时,

\[f(S,u,k)=\sum_{\min(i,j)=k}f(S,lson_u,i)\times f(S,rson_i,j) \]

> 同理,? 就是两种情况都算。我们把 \(m\) 个元素的集合状压,并算出所有 \(S\) 的答案,后面查表即可。这里复杂度 \(\mathcal O(2^m|E|)\)

然后我们得到 \(g(S_i)=f(S_i,root,1)\) 代表比 \(a_i\) 小的元素集合为 \(S\) 时答案大于等于 \(a_i\) 的方案数。考虑容斥得到最终答案,我们把 \(\{a\}\) 从小到大排序得到下标排列 \(\{b\}\),答案为:

\[\sum_{i=1}^m(a_{b_i}-a_{b_{i-1}})\times g(S_i) \]

这样总复杂度就是 \(\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\) 是合数,不一定能求逆元。对于

\[f_{n-1}a\equiv -f_nb\pmod m \]

约去 \(\gcd(a,-b,m)\) 得到

\[f_{n-1}a'\equiv f_nb'\pmod{m'} \]

但是此时 \(m'\) 对于四者依然不一定互质。然后由于 \(\gcd(f_n,f_{n-1})=\gcd(n,n-1)=1\),设

\[\begin{aligned} p&=\gcd(a',m')=\gcd(f_n,m')\\ q&=\gcd(b',m')=\gcd(f_{n-1},m') \end{aligned} \]

整个式子再都约去 \(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';
	}
}
posted @ 2021-04-04 20:50  RenaMoe  阅读(108)  评论(0)    收藏  举报