OI笔记 | 2023.7-11 做题记录(二)

k 小值

有两个长度为 $n$ 的数组 $a,b$,数组内的元素单调不降。

$q$ 次操作,操作有两种:

  1. 单点修改:$a_x\gets y$ 或 $b_x\gets y$。保证修改后的数组内的元素仍单调不降。

  2. 查询:将 $a$ 数组的 $[l_1,r_1]$ 内的元素和 $b$ 数组的 $[l_2, r_2]$ 内的元素合并在一起,查询这些元素的第 $k$ 小值。

$1\leq n\leq 5\times 10^5, 1\leq q,a_i,b_i\leq 10^6$

题解

Solution 1

$\mathcal{O}(q\log n\log a_i)$,考虑二分答案 $mid$,在两个数组中二分查找比它小的数有多少个。此方法无法通过本题。

int L = 0, R = MAX_V;
while(L < R - 1) {
  int mid = L + ((R - L) >> 1);
  int pos1 = lower_bound(a + l1, a + r1 + 1, mid) - a - l1;
  int pos2 = lower_bound(b + l2, b + r2 + 1, mid) - b - l2;
  if(pos1 + pos2 >= k)	R = mid;
  else	L = mid;	
}

Solution 2

$\mathcal{O}(q\log n)$,考虑分治(或者说倍增的一种递归实现)。设答案为 $f(l_1,r_1,l_2,r_2,k)$,则我们令 $s=l_1+\lfloor \frac{k}{2}\rfloor -1, t = l_2+\lfloor \frac{k}{2}\rfloor -1$,若 $a_s<b_t$,则 $a_{l_1},a_{l_2},\dots a_{s}$ 都不可能成为答案,所以 $f(l_1,r_1,l_2,r_2,k)=f(s+1,r_1,l_2,r_2,k-(s-l_1+1))$。若 $a_s\geq b_t$ 同理。

每次都可以舍掉其中一个区间的一半,因此时间复杂度可以保证通过本题。递归实现即可,注意边界处理。

int f(int l1, int r1, int l2, int r2, int k) {
	if(l1 > r1)	return b[l2 + k - 1];
	if(l2 > r2)	return a[l1 + k - 1];
	if(k == 1)	return min(a[l1], b[l2]);
	int s = min(l1 + (k >> 1) - 1, r1), t = min(l2 + (k >> 1) - 1, r2);
	if(a[s] < b[t])	return f(s + 1, r1, l2, r2, k - (s - l1 + 1));
	else	return f(l1, r1, t + 1, r2, k - (t - l2 + 1));
}

[USACO09NOV] Lights G

洛谷 P2962

给出一张 $n$ 个点 $m$ 条边的无向图,每个点的初始状态都为 $0$。

你可以操作任意一个点,操作结束后该点以及所有与该点相邻的点的状态都会改变,由 $0$ 变成 $1$ 或由 $1$ 变成 $0$。

你需要求出最少的操作次数,使得在所有操作完成之后所有 $n$ 个点的状态都是 $1$。

$1\le n\le35,1\le m\le595, 1\le a,b\le n$。

题解

经典开关问题的无向图上版本。

显然可以枚举搜索每个点选不选,也可以高斯消元。

这里我们使用搜索,但是 $2^{35}$ 无法接受,所以可以使用 Meet in the middle 思想,把节点拆成 $1\sim \lfloor \frac{n}{2}\rfloor $ 和 $(\lfloor \frac{n}{2}\rfloor + 1)\sim n$ 两部分,分别进行搜索,并且考虑搜索的同时把两边的状态合并。

时间复杂度 $\mathcal{O}(n2^{\frac{n}{2}})$。

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;

const int MAX_N = 40;
int n, m, ans;
ll aim, v[MAX_N];
map<ll, int> step;

void dfs(int u, int r, ll state, int cnt) {
	if(step.count(state))	step[state] = min(step[state], cnt);
	else	step[state] = cnt;
	if(state == aim)	ans = min(ans, cnt);
	if(u > (n >> 1) && step.count(state ^ aim))	ans = min(ans, step[state ^ aim] + cnt);
	if(u > r)	return;
	dfs(u + 1, r, state, cnt);
	dfs(u + 1, r, (state ^ v[u]), cnt + 1);
}

int main() {
	n = read(), m = read();
	for(int i = 1; i <= n; i++)
		v[i] |= (1ll << i);
	for(int i = 1; i <= m; i++) {
		int a = read(), b = read();
		v[a] |= (1ll << b);
		v[b] |= (1ll << a);
	}
	aim = (((1ll << n) - 1) << 1), ans = n;
	dfs(1, (n >> 1), 0, 0);
	dfs((n >> 1) + 1, n, 0, 0);
	write(ans), putchar('\n');
	return 0;
}

[USACO18OPEN] Talent Show G

洛谷 P4377

$n$ 头奶牛,第 $i$ 头奶牛重量为 $w_i$,才艺水平为 $t_i$。

从中选一些参加比赛的奶牛。这些奶牛的总重量至少为 $W$。

最大化选出奶牛的才艺和与重量和的比值。

$1 \leq n \leq 250$,$1 \leq W \leq 1000$,$1 \leq w_i \leq 10^6$,$1 \leq t_i \leq 10^3$。

题解

01分数规划板子。

题意相当于最大化 $\frac{\sum p_i t_i}{\sum p_i w_i}$,其中 $p_i\in \left{ 0,1\right}$。二分答案 $x$,则答案合法,当且仅当 $\frac{\sum p_i t_i}{\sum p_i w_i}> x\iff \sum p_i(t_i-xw_i)>0$。所以转化成选出一些奶牛,重量为 $w_i$,价值为 $t_i-xw_i$,且满足重量和至少为 $W$。跑一个 01 背包即可,则式子左边的最大值为 $dp_{W}$。

时间复杂度 $\mathcal{O}(nW\log W)$。

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;

inline ll read() {...}
char buf[50];
inline void write(ll x) {...}

const int MAX_N = 255, MAX_W = 1005;
const double MAX_ANS = 1e3 + 1, eps = 1e-4, INF = 1e9;
int n, W, w[MAX_N], t[MAX_N];
double dp[MAX_W];

bool check(double x) {
	for(int i = 1; i <= W; i++)
		dp[i] = -INF;
	for(int i = 1; i <= n; i++)
		for(int j = W; j >= 0; j--) {
			int k = min(W, j + w[i]);
			dp[k] = max(dp[k], dp[j] + t[i] - x * w[i]);
		}
	return dp[W] > 0;
}

int main() {
	n = read(), W = read();
	for(int i = 1; i <= n; i++)
		w[i] = read(), t[i] = read();
	double l = 0, r = MAX_ANS;
	while(r - l > eps) {
		double mid = (l + r) * 0.5;
		if(check(mid))	l = mid;
		else	r = mid;
	}
	write(int(1000 * l));
	return 0;
}

线段树优化建图

CF786B

有一个 $n$ 个点的有向图,初始没有点有连边。

$q$ 次操作,操作有三种:

1 v u w:连边 $(v,u,w)$。

2 v l r w:$\forall \ l\leq u\leq r$,连边 $(v,u,w)$。

3 v l r w:$\forall\ l\leq u\leq r$,连边 $(u,v,w)$。

在所有操作后,求 $s$ 到所有点的最短距离。

$1\leq n,q\leq 10^5, 1\leq w\leq 10^9$

题解

如果暴力建边,时空复杂度无法接受。由于有区间操作,我们可以考虑使用线段树来优化。

我们可以把线段树上的节点纳入原图中考虑,原图中的点 $1\sim n$ 对应线段树的叶子节点,线段树的其他节点对应一些区间。

建立两棵线段树,一个入树,一个出树。对于入树,从左右儿子向父亲连零边;对于出树,从父亲向左右儿子连零边。并且,我们把入树和出树的相同的叶子节点相互连零边。这样以来,最后的最短路就是从 $s$ 对应入树的叶子节点出发,到达出树的叶子节点的距离。

那么操作 $2$ 相当于 $v$ 对应入树的叶子节点向区间 $[l,r]$ 对应出树的节点连边;操作 $3$ 同理。

#include <bits/stdc++.h>
#define pb emplace_back
using namespace std;
typedef long long ll;
typedef pair<ll, int> pii;

inline ll read() {...}
char buf[50];
inline void write(ll x) {...}

const int MAX_N = 3e6 + 10, DELTA = 5e5;
const ll INF = (1ll << 60);
int n, q, s, cnt, leaf1[MAX_N], leaf2[MAX_N], fir[MAX_N];
bool vis[MAX_N];
ll dis[MAX_N];

struct edge {
	int v, w, nxt;
	edge() { }
	edge(int _v, int _w, int _nxt) :
		v(_v), w(_w), nxt(_nxt) { }
} e[MAX_N << 2];

void AddEdge(int u, int v, int w) {
	e[++cnt] = edge(v, w, fir[u]);
	fir[u] = cnt;
}

inline int lc(int p) { return (p << 1); }
inline int rc(int p) { return (p << 1) | 1;}
inline int mid(int s, int t) { return s + ((t - s) >> 1); }
void build_tree(int s, int t, int p) {
	if(s == t) {
		leaf1[s] = p;
		leaf2[s] = p + DELTA;
		AddEdge(leaf1[s], leaf2[s], 0);
		AddEdge(leaf2[s], leaf1[s], 0);
		return;
	}
	int m = mid(s, t);
	AddEdge(p, lc(p), 0), AddEdge(p, rc(p), 0);
	AddEdge(lc(p) + DELTA, p + DELTA, 0), AddEdge(rc(p) + DELTA, p + DELTA, 0);
	build_tree(s, m, lc(p));
	build_tree(m + 1, t, rc(p));
}

void update(int s, int t, int p, int l, int r, int u, int type, int w) {
	if(l <= s && t <= r) {
		if(type == 2)	AddEdge(leaf2[u], p, w);
		else	AddEdge(p + DELTA, leaf1[u], w);
		return;
	}
	int m = mid(s, t);
	if(l <= m)	update(s, m, lc(p), l, r, u, type, w);
	if(r > m)	update(m + 1, t, rc(p), l, r, u, type, w);
} 

void dijkstra(int s) {
	priority_queue<pii, vector<pii>, greater<pii>> pq;
	for(int i = 1; i < MAX_N; i++)
		dis[i] = INF;
	pq.push(make_pair(0, s));
	dis[s] = 0;
	while(!pq.empty()) {
		int u = pq.top().second; pq.pop();
		if(vis[u])	continue;
		vis[u] = 1;
		for(int i = fir[u]; i; i = e[i].nxt) {
			int v = e[i].v, w = e[i].w;
			if(dis[u] + w < dis[v]) {
				dis[v] = dis[u] + w;
				pq.push(make_pair(dis[v], v));
			}
		}
	}
}

int main() {
	n = read(), q = read(), s = read();
	build_tree(1, n, 1);
	while(q--) {
		int type = read();
		if(type == 1) {
			int v = read(), u = read(), w = read();
			AddEdge(leaf2[v], leaf1[u], w);
		}
		else {
			int v = read(), l = read(), r = read(), w = read();
			update(1, n, 1, l, r, v, type, w);
		}
	}
	dijkstra(leaf2[s]);
	for(int i = 1; i <= n; i++)
		write(dis[leaf1[i]] == INF ? -1 : dis[leaf1[i]]), putchar(" \n"[i == n]);
	return 0;
}

Bonus

洛谷 P6348:要求支持区间 $[a,b]$ 向区间 $[c,d]$ 连边。

这个可以通过建两个虚点 $x,y$,然后在入树将 $[a,b]$ 连到 $x$,在出树将 $y$ 连到 $[c,d]$,然后再连边 $(x,y,w)$ 即可。

[APIO2008] 免费道路

洛谷 P3623

有一个 $N$ 个点,$M$ 条边的无向图,边有黑白两种。要求找出原图的一个生成树,使得树上正好有 $K$ 条白边。

$1\leq N\leq 2\times 10^4,1\leq M\leq 10^5, 0\leq K \leq N - 1$

题解

先按黑边优先来排序,跑一遍 Kruskal,求出加入的白边数量的最小值 $p$,以及此时加入了哪些白边。若 $p>K$,则无解。

再跑一遍 Kruskal,这次优先加入那些必须要加的白边,然后再加其他白边直到达到 $K$ 条,其次再加其他黑边。若白边达不到 $K$ 条或加完之后无法形成生成树,则无解。

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int, int> pii;

inline ll read() {...}
char buf[50];
inline void write(ll x) {...}

const int MAX_N = 1e5 + 10;
int n, m, k, cnt, tot;
struct edge { int u, v, w; } E[MAX_N], ans[MAX_N];

struct dsu {
	int fa[MAX_N];
	dsu(int n) {
		for(int i = 1; i <= n; i++)
			fa[i] = i;
	}
	int find(int x) { return x == fa[x] ? x : fa[x] = find(fa[x]); }
	bool merge(int x, int y) {
		int fx = find(x), fy = find(y);
		if(fx == fy)	return 0;
		fa[fx] = fy;
		return 1;
	}
};

void solve() {
	sort(E + 1, E + m + 1, [](edge x, edge y) { return x.w > y.w; });
	dsu D = dsu(n);
	for(int i = 1; i <= m; i++) {
		bool f = D.merge(E[i].u, E[i].v); tot += f;
		if(f && E[i].w == 0)	cnt++, E[i].w = -1;
	}
	if(cnt > k || tot != n - 1) { puts("no solution"); return; }
	sort(E + 1, E + m + 1, [](edge x, edge y) { return x.w < y.w; });
	D = dsu(n);
	cnt = tot = 0;
	for(int i = 1; i <= m; i++) {
		if(cnt == k && E[i].w <= 0)	continue;
		bool f = D.merge(E[i].u, E[i].v);
		if(f)	ans[++tot] = E[i];
		if(f && E[i].w <= 0)	cnt++;
	}
	if(cnt < k || tot != n - 1) { puts("no solution"); return; }
	for(int i = 1; i <= tot; i++) {
		write(ans[i].u), putchar(' ');
		write(ans[i].v), putchar(' ');
		write(max(ans[i].w, 0)), putchar('\n');
	}
}

int main() {
	n = read(), m = read(), k = read();
	for(int i = 1; i <= m; i++)
		E[i].u = read(), E[i].v = read(), E[i].w = read();
	solve();
	return 0;
}

[ARC126C] Maximize GCD

ARC126C

给定一个序列 $A$,每次操作可以使 $A_i + 1$ ($i \in [1, n]$,$K$ 次操作的 $i$ 可以不同),最多可以做 $K$ 次。问 $\gcd{A_1, A_2, …, A_n}$ 的最大值。

$ 2\leq\ N\leq\ 3\times\ 10^5 , 1\leq\ K\leq\ 10^{18} ,1\ \leq\ A_i\leq\ 3\times\ 10^5 $

题解

设序列 $A$ 中的最大值为 $t$。

当 $K\geq \sum\limits_{i=1}^n (t - A_i)$ 时,显然我们可以把所有数先加到 $t$,然后把剩下的操作次数平均分给每一个数,得到最大的答案。

否则,答案一定小于 $t$,我们就从 $t$ 到 $1$ 枚举 $ans$,检验它的合法性。要让所有数的 $\gcd$ 等于 $ans$,我们就让每个数加到它以上的最接近的 $ans$ 的倍数,所以需要的最少次数为 $\sum\limits_{i=1}^n (\lceil \frac{A_i}{ans} \rceil\times ans - A_i)$,判断它与 $K$ 的大小关系即可。

考虑加速上面式子的计算。我们发现 $\lceil \frac{A_i}{ans} \rceil$ 的取值是连续的,对于 $A_i\in ((k-1)\cdot ans, k\cdot ans]$,取值均为 $k$。所以把 $A_i$ 按值域分成 $\lceil \frac{t}{ans} \rceil$ 段,每一段的答案为 $k\cdot ans\cdot cnt-sum$,其中 $cnt$ 为区间中 $A_i$ 的数量,$sum$ 为区间中 $A_i$ 的和,这些都可以预处理。

所以最终的时间复杂度为 $\mathcal{O}(n \ln n)$。

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;

inline ll read() {...}
inline void write(ll x) {...}

const int MAX_N = 6e5 + 10;
ll n, K, delta, a[MAX_N], cnt[MAX_N], sum[MAX_N];

bool check(int ans) {
    ll p = 0;
    for(ll k = 1; k <= (a[n] + ans) / ans; k++) {
        ll l = ans * (k - 1), r = ans * k;
        p += k * ans * (cnt[r] - cnt[l]) - (sum[r] - sum[l]);
    }
    return p <= K;
}

int main() {
    n = read(), K = read();
    for(int i = 1; i <= n; i++)
        a[i] = read();
    sort(a + 1, a + n + 1);
    for(int i = 1; i <= n; i++)
        delta += a[n] - a[i];
    if(delta <= K)  write(a[n] + (K - delta) / n);
    else {
        for(int i = 1; i <= n; i++)
            cnt[a[i]]++;
        for(int i = 1; i <= 2 * a[n]; i++)
            sum[i] = sum[i - 1] + cnt[i] * i, cnt[i] += cnt[i - 1];
        int ans = a[n] - 1;
        for(; ans >= 1; ans--)
            if(check(ans))  break;
        write(ans), putchar('\n');
    }
    return 0;
}

二分图完美匹配方案数

有一个左部和右部均为 $n$ 个点的二分图,求它的完美匹配方案数。

这个可以状压 dp,设 $dp[i][S]$ 为考虑到左部的第 $i$ 个点,右部的选择情况为状态 $S$ 的方案数。显然 $i=\text{popcount}(S)$,所以可以省略掉第一维。有转移方程如下:

\[dp[S]=\sum dp[S\oplus 2^j]\]

其中 $j$ 为在 $S$ 中选中的右部点,且需要满足 $i$ 和 $j$ 有连边。

n = read();
for(int i = 0; i < n; i++)
  for(int j = 0; j < n; j++)
    a[i][j] = read();
S = (1 << n) - 1;
dp[0] = 1;
for(int s = 1; s <= S; s++) {
  int i = __builtin_popcount(s) - 1;
  for(int j = 0; j < n; j++)
    if(a[i][j] && ((s >> j) & 1))   (dp[s] += dp[s ^ (1 << j)]) %= MOD;
}
write(dp[S]); 

Count Bracket Sequences

ABC312D

给定一个非空字符串包含 ()?

其中 ? 需要用 ( 或者 ) 替换掉,求替换后的字符串是合法的括号串的方案数对 $ 998244353$ 取模的值。

题解

对于合法括号序列的判定问题,我们往往将左括号视为 $1$,右括号视为 $-1$,则序列为合法括号序列的充要条件为:

  1. 序列所有数之和为 $0$。

  2. 序列的所有前缀和都非负。

所以考虑 $f_{i,j}$ 表示考虑前 $i$ 个数,前缀和为 $j$ 的方案数。

如果第 $i$ 位是左括号,则 $f_{i,j}=\sum f_{i-1,j-1}$。

如果第 $i$ 位是右括号,则 $f_{i,j}=\sum f_{i-1,j+1}$。

如果第 $i$ 位是问号,则 $f_{i,j}=\sum f_{i-1,j-1} + \sum f_{i-1,j+1}$

时间复杂度:$\mathcal{O}(n^2)$。

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;

inline ll read() {...}
inline void write(ll x) {...}

const int MAX_N = 3005, MOD = 998244353;
char s[MAX_N];
int n, dp[MAX_N][MAX_N];

int main() {
	scanf("%s", s + 1);
	n = strlen(s + 1);
	dp[0][0] = 1;
	for(int i = 1; i <= n; i++) {
		if(s[i] == '(') {
			for(int j = 1; j <= i; j++)
				(dp[i][j] += dp[i - 1][j - 1]) %= MOD;
		}
		else if(s[i] == ')') {
			for(int j = 0; j < i; j++)
				(dp[i][j] += dp[i - 1][j + 1]) %= MOD;
		}
		else {
			for(int j = 1; j <= i; j++)
				(dp[i][j] += dp[i - 1][j - 1]) %= MOD;			
			for(int j = 0; j < i; j++)
				(dp[i][j] += dp[i - 1][j + 1]) %= MOD;
		}
	}
	write(dp[n][0]);
	return 0;
}

Company

CF1062E

给定一颗树,有若干个询问,每个询问给出 $l$,$r$,要求编号为 $l$~$r$ 的点任意删去一个之后剩余点的 LCA 深度最大,输出删去点的编号和 LCA 的最大深度。

题解

经典小 Trick:一个点集内的点的 LCA 为他们之中 dfn 最大和 dfn 最小的点的 LCA。

所以,只有当删的点是 dfn 最大或最小的点时,LCA 才有可能改变。分两类讨论,用 ST 表维护 dfn 序的最大最小值的位置即可。

#include <bits/stdc++.h>
#define pb emplace_back 
using namespace std;
typedef long long ll;

inline ll read() {...}
inline void write(ll x) {...}

const int MAX_N = 100005, MAX_LOG = 18;
int n, q, dfncnt, dep[MAX_N], dfn[MAX_N], fa[MAX_N][MAX_LOG];
int log_2[MAX_N], stmin[MAX_N][MAX_LOG], stmax[MAX_N][MAX_LOG];
vector<int> G[MAX_N];

int Min(int x, int y) { return dfn[x] < dfn[y] ? x : y; }
int Max(int x, int y) { return dfn[x] > dfn[y] ? x : y; }

int qmin(int l, int r) {
	int len = log_2[r - l + 1];
	return Min(stmin[l][len], stmin[r - (1 << len) + 1][len]);
}

int qmax(int l, int r) {
	int len = log_2[r - l + 1];
	return Max(stmax[l][len], stmax[r - (1 << len) + 1][len]);
} 

void pre(int N) {
	log_2[1] = 0, log_2[2] = 1;
	for(int i = 3; i <= N; i++)
		log_2[i] = log_2[i >> 1] + 1;
}

void dfs(int u) {
	dfn[u] = ++dfncnt;
	dep[u] = dep[fa[u][0]] + 1;
	for(int i = 1; i < MAX_LOG; i++)
		fa[u][i] = fa[fa[u][i - 1]][i - 1];
	for(int v : G[u])
		dfs(v);
}

int lca(int x, int y) {
	if(!x || !y)	return x + y;
	if(dep[x] < dep[y])	swap(x, y);
	for(int i = MAX_LOG - 1; i >= 0; i--)
		if(fa[x][i] && dep[fa[x][i]] >= dep[y])	x = fa[x][i];
	if(x == y)	return x;
	for(int i = MAX_LOG - 1; i >= 0; i--)
		if(fa[x][i] && fa[y][i] && fa[x][i] != fa[y][i])	x = fa[x][i], y = fa[y][i];
	return fa[x][0];
}

int cal(int l, int mid, int r) {
	int lca1, lca2;
	if(l < mid)	lca1 = lca(qmin(l, mid - 1), qmax(l, mid - 1)); 
	if(mid < r)	lca2 = lca(qmin(mid + 1, r), qmax(mid + 1, r));
	if(mid == r)	return lca1;
	if(mid == l)	return lca2;
	return lca(lca1, lca2);
}

int main() {
	n = read(), q = read();
	pre(n);
	for(int i = 2; i <= n; i++) {
		fa[i][0] = read(); 
		G[fa[i][0]].pb(i);
	}
	dfs(1);
	for(int i = 1; i <= n; i++)
		stmin[i][0] = stmax[i][0] = i; 
	for(int j = 1; j < MAX_LOG; j++)
		for(int i = 1; i + (1 << j) - 1 <= n; i++) {
			stmin[i][j] = Min(stmin[i][j - 1], stmin[i + (1 << (j - 1))][j - 1]);
			stmax[i][j] = Max(stmax[i][j - 1], stmax[i + (1 << (j - 1))][j - 1]);
		}
	while(q--) {
		int l = read(), r = read();
		int mn = qmin(l, r), mx = qmax(l, r);
		int ans1 = cal(l, mn, r), ans2 = cal(l, mx, r);
		if(dep[ans1] > dep[ans2])	write(mn), putchar(' '), write(dep[ans1] - 1);
		else	write(mx), putchar(' '), write(dep[ans2] - 1);
		putchar('\n');
	}
}

[SCOI2016] 萌萌哒

一个长度为 $n$ 的大数,用 $S_1S_2S_3 \cdots S_n$表示,其中 $S_i$ 表示数的第 $i$ 位, $S_1$ 是数的最高位。有 $m$ 个限制条件,每个条件表示为四个数,$l_1,r_1,l_2,r_2$,即两个长度相同的区间,表示子串$S_{l_1}S_{l_1+1}S_{l_1+2} \cdots S_{r_1}$与$S_{l_2}S_{l_2+1}S_{l_2+2} \cdots S_{r_2}$完全相同。

求有多少个满足所有条件的数。

题解

一个朴素的想法是把区间的对应转化为点的对应,然后用并查集把对应的相同的点合并起来,最后答案即为 $9\times 10^{C-1}$,其中 $C$ 为集合个数。

然后你发现集合的合并具有可加性。用倍增的思想,把长为 $L$ 的区间分成若干长度为 $2$ 的幂的区间,创建 $\lfloor \log_2 n\rfloor$ 层并查集,设 $fa(x, k)$ 为以 $x$ 为左端点,长度为 $2^k$ 的区间的那一层并查集的父亲。这样合并就是 $\mathcal{O}(\log n)$ 的。

然后最后我们想看 $k=0$ 的那层并查集有多少个集合,所以下放一下,从第 $k$ 层的对应关系推到第 $k-1$ 层。最后统计答案即可。

#include <bits/stdc++.h>
#define pb emplace_back 
using namespace std;
typedef long long ll;
typedef pair<int, int> pii;

inline ll read() {...}
inline void write(ll x) {...}

const int MAX_N = 1e5 + 10, MAX_LOG = 17, MOD = 1e9 + 7;
int n, m, log_2[MAX_N];

void init_log2(int n) {
	log_2[1] = 0;
	for(int i = 1; i <= n; i++)
		log_2[i] = log_2[i >> 1] + 1; 
}

struct dsu {
	int fa[MAX_N][MAX_LOG];
	dsu(int n) {
		for(int i = 1; i <= n; i++)
			for(int j = 0; j < MAX_LOG; j++)
				fa[i][j] = i;
	}
	int find(int x, int k) {
		if(fa[x][k] == x)	return x;
		return fa[x][k] = find(fa[x][k], k);
	} 
	void merge(int x, int y, int k) {
		int fx = find(x, k), fy = find(y, k);
		fa[fx][k] = fy;
	}
};

int main() {
	n = read(), m = read();
	init_log2(n);
	dsu D = dsu(n);
	for(int i = 1; i <= m; i++) {
		int l1 = read(), r1 = read(), l2 = read(), r2 = read();
		for(int k = MAX_LOG - 1; k >= 0; k--)
			if(l1 + (1 << k) - 1 <= r1) {
				D.merge(l1, l2, k);
				l1 += (1 << k), l2 += (1 << k);
			}
	}
	for(int k = MAX_LOG - 1; k >= 1; k--)
		for(int i = 1; i + (1 << k) - 1 <= n; i++) {
			int I = D.find(i, k);
			D.merge(i, I, k - 1), D.merge(i + (1 << (k - 1)), I + (1 << (k - 1)), k - 1);
		}
	ll ans = 1;
	for(int i = 1; i <= n; i++)
		if(D.fa[i][0] == i)	ans = 1ll * ans * (ans == 1 ? 9 : 10) % MOD;
	write(ans), putchar('\n');
	return 0;
}

Joy © 2023 Powered by Jekyll and Theme by solid

今日诗词API Valine 评论管理 Github