练习选讲(2023.5-6)

5 月

NOIP 2018 模拟 Day2:

ending:给定一棵 \(n\) 个点的树,边权 \(w\in\{0,1\}\)。求出有多少个三元组 \((i,j,k)\),满足 \(dist(i,j),dist(i,k)>0\)\(1\le n\le 10^5\)

题解:

\(w(i,j)=0\),则将 \(i,j\) 合并进一个连通块内。对于一个连通块,设其大小为 \(size\),则对于该连通块内的一个节点 \(i\),满足 \(dist(i,j)=0\)\(dist(i,k)=0\) 的三元组 \((i,j,k)\) 的数量为 \((size-1)(size-2)+2(size-1)(n-size)\)

那么 \(ans=n(n-1)(n-2)-\sum size((size-1)(size-2)+2(size-1)(n-size))\)

时间复杂度 \(O(n\log n)\)

点击查看代码
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <vector>
#include <queue>

using namespace std;

#define int long long 

const int N = 1e5+10;

int n, ans, p[N], siz[N];

int check(int w) {
	while (w) {
		if (w % 10 != 4 && w % 10 != 7) return 0;
		w /= 10;
	}
	return 1;
}

int find(int x) {
	if (p[x] != x) return find(p[x]);
	return x;
}

void merge(int u, int v) {
	int fu = find(u), fv = find(v);
	if (fu == fv) return ;
	siz[fv] += siz[fu], p[fu] = fv;
	return ;
}

signed main() {
	// freopen("ending.in", "r", stdin);
	// freopen("ending.out", "w", stdout);
	
	scanf("%lld", &n);

	for (int i = 1; i <= n; ++i) p[i] = i, siz[i] = 1;

	
	int u, v, w;
	for (int i = 1; i < n; ++i) {
		scanf("%lld%lld%lld", &u, &v, &w);
		w = check(w);
		if (!w) merge(u, v); // 分割成内部不能到达的连通块
	}

	for (int i = 1; i <= n; ++i) {
		if (i != find(i) || siz[i] < 2) continue ;
		int size = siz[i];
		ans += size*(size-1)*(size-2) + size*(size-1)*(n-size)*2;
	}
	printf("%lld\n", n*(n-1)*(n-2)-ans);
	return 0;
}

/*
10
1 2 8
1 3 7
3 4 47
5 7 23
4 6 4
6 10 747
8 6 57
9 3 447
9 5 88

566
*/

6 月

6.9:

P1486 [NOI2004] 郁闷的出纳员:* 平衡树(蓝)

题解:

用一个变量 \(delta\) 记录员工们的工资变化量。

对于插入操作 I k,向平衡树中插入一个数 \(k-delta\)(其他人都增加了 \(delta\),但他没有增加,相当于其他人不增加,他减小 \(delta\))。

对于全局加法操作 A k,直接将 \(delta\) 增加 \(k\) 即可。

对于全局减法操作 S k,将 \(delta\) 减少 \(k\),同时删除平衡树中小于 \(min-delta\) 的数(与插入操作思想类似)。

对于查询排名操作 F k,在平衡树上二分查找即可。若遍历到 \(0\) 号节点则返回 \(-1\)

用 fhq-treap 实现,时间复杂度 \(O(n\log n)\)

点击查看代码
#include <iostream>
#include <cstdio>
#include <algorithm>

using namespace std;

const int N = 3e5+10;

int n, minl, ans, delta, idx, root; // delta 表示所有员工的工资变化量

struct Node {
	int l, r;
	int val, key; // BST, 堆
	int size; 
} tree[N];

void pushup(int u) {
	tree[u].size = tree[tree[u].l].size + tree[tree[u].r].size + 1;
}

int newNode(int v) {
	++ idx;
	tree[idx].l = tree[idx].r = 0;
	tree[idx].val = v, tree[idx].key = rand();
	tree[idx].size = 1;
	return idx;
}

void split(int p, int v, int &x, int &y) { // 按 v 分裂子树, 左子树小于等于x, 右子树大于x 
	if (!p) {x = y = 0; return ;}
	if (tree[p].val <= v) {
		x = p;
		split(tree[p].r, v, tree[p].r, y);
	} else {
		y = p;
		split(tree[p].l, v, x, tree[p].l);
	}
	pushup(p);
}

int merge(int x, int y) { // 合并子树, x中的值均小于y中的值 
	if (!x || !y) return x + y;
	if (tree[x].key <= tree[y].key) {
		tree[x].r = merge(tree[x].r, y);
		pushup(x); return x;
	} else {
		tree[y].l = merge(x, tree[y].l);
		pushup(y); return y;
	}
}

void insert(int v) { // 插入 v 
	int x, y, z;
	split(root, v, x, z);
	y = newNode(v);
	root = merge(merge(x, y), z);
}

void del(int v) { // 删除小于v的数 
	int x, y;
	split(root, v-1, x, y);
	ans += tree[x].size;
	root = y; 
}

int get_num(int p, int k) { // 在以p为根的子树内查询第 k 大数 
	if (!p) return -1;
	if (tree[tree[p].r].size >= k) return get_num(tree[p].r, k);
	if (tree[tree[p].r].size+1 == k) return tree[p].val;
	return get_num(tree[p].l, k-tree[tree[p].r].size-1);
}

int main() {
	ios::sync_with_stdio(0);
	cin.tie(0), cout.tie(0); 
	
	cin >> n >> minl;
		
	char op; int k;
	while (n -- ) {
		cin >> op >> k;
		if (op == 'I') {
			if (k < minl) continue;
			insert(k-delta); 
		} else if (op == 'A') {
			delta += k;
		} else if (op == 'S') {
			delta -= k;
			del(minl-delta);
		} else {
			int t = get_num(root, k);
			cout << ((t == -1) ? t : t+delta) << '\n';
		}
	}
	
	cout << ans << '\n';
	return 0;
} 

6.10:

P1110 [ZJOI2007] 报表统计:平衡树(蓝)

我们可以记录原数组第 \(i\) 个数的开头 \(a_i\) 和被插入的结尾 \(b_i\)。用两个平衡树分别维护相邻两个数的差值和数组中所有的数,另外用一个变量 \(ans\) 存储当前答案。

对于插入操作 Insert i k,那么需要在第一棵平衡树中删除 \(|a_{i+1}-b_i|\),再插入 \(|k-b_i|\)\(|a_{i+1}-k|\)。在第二棵平衡树中查找 \(k\) 的前驱 \(pre\) 及后继 \(suf\)(可能与 \(k\) 相等),则 \(ans=\min\{ans,|k-pre|,|k-suf|\}\)。更新 \(b_i=k\)

对于查找操作 MIN_GAP,直接在第一棵平衡树中找最小值即可。对于查找操作 MIN_SORT_GAP,直接输出 \(ans\) 即可。

使用 fhq-treap 实现,时间复杂度 \(O(n\log n)\)

点击查看代码
#include <iostream>
#include <cstdio>
#include <algorithm>

using namespace std;

const int N = 1e6+10;

int n, m, ans = (int)1e9, a[N], b[N];

struct Node {
	int l, r;
	int val, key;
	int size;
};

class FHQ_Treap {
private:
	int root, idx;
	Node tree[N];
public:
	void pushup(int u) {
		tree[u].size = tree[tree[u].l].size + tree[tree[u].r].size + 1;
	}
	
	int newNode(int v) {
		++ idx;
		tree[idx].l = tree[idx].r = 0;
		tree[idx].val = v, tree[idx].key = rand();
		tree[idx].size = 1;
		return idx;
	}
	
	void split(int p, int v, int &x, int &y) {
		if (!p) {x = y = 0; return ;}
		if (tree[p].val <= v) {
			x = p;
			split(tree[p].r, v, tree[p].r, y);
		} else {
			y = p;
			split(tree[p].l, v, x, tree[p].l);
		}
		pushup(p);
	}
	
	int merge(int x, int y) {
		if (!x || !y) return x + y;
		if (tree[x].key <= tree[y].key) {
			tree[x].r = merge(tree[x].r, y);
			pushup(x); return x;
		} else {
			tree[y].l = merge(x, tree[y].l);
			pushup(y); return y;
		}
	}
	
	void insert(int v) {
		int x, y, z;
		split(root, v, x, z);
		y = newNode(v);
		root = merge(merge(x, y), z);
	}
	
	void del(int v) {
		int x, y, z;
		split(root, v, x, z), split(x, v-1, x, y);
		y = merge(tree[y].l, tree[y].r);
		root = merge(merge(x, y), z);
	}
	
	int get_num(int p, int k) {
		if (!p) return 1e9;
		if (tree[tree[p].l].size >= k) return get_num(tree[p].l, k);
		else if (tree[tree[p].l].size+1 == k) return tree[p].val;
		else return get_num(tree[p].r, k-tree[tree[p].l].size-1);
	}
	
	int get_pre(int v) {
		int x, y;
		split(root, v-1, x, y);
		int ans = get_num(x, tree[x].size);
		root = merge(x, y);
		return ans;
	}
	
	int get_suf(int v) {
		int x, y;
		split(root, v-1, x, y);
		int ans = get_num(y, 1);
		root = merge(x, y);
		return ans;
	}
	
	int min_gap() {
		return get_num(root, 1);
	} 
} Tree1, Tree2;

int main() {
	ios::sync_with_stdio(0);
	cin.tie(0), cout.tie(0);
	
	cin >> n >> m;
	for (int i = 1; i <= n; ++i) {
		cin >> a[i]; b[i] = a[i];
		if (i > 1) Tree1.insert(abs(a[i]-a[i-1]));
		ans = min(ans, min(abs(a[i]-Tree2.get_pre(a[i])), abs(a[i]-Tree2.get_suf(a[i]))));
		Tree2.insert(a[i]);
	}
	
	string op; int i, k;
	while (m -- ) {
		cin >> op;
		if (op == "INSERT") {
			cin >> i >> k;
			if (i < n) Tree1.del(abs(a[i+1]-b[i])), Tree1.insert(abs(a[i+1]-k));
			Tree1.insert(abs(k-b[i]));
			b[i] = k;
			ans = min(ans, min(abs(k-Tree2.get_pre(k)), abs(k-Tree2.get_suf(k))));
			Tree2.insert(k);
		} else if (op == "MIN_GAP") {
			cout << Tree1.min_gap() << '\n';
		} else {
			cout << ans << '\n';
		}
	}
	
	return 0;
}

P3391 【模板】文艺平衡树:* 文艺平衡树(蓝)

按大小分裂子树的 fhq-treap。

点击查看代码
#include <iostream>
#include <cstdio>
#include <algorithm>

using namespace std;

const int N =  1e5+10;

int n, m;
int root, idx;

struct Node {
	int l, r;
	int val, key;
	int size;
	// 如果我们以BST的中序遍历作为答案,那么对于区间[l,r],只需交换其对应子树中所有节点的左右儿子 
	int tag; // 翻转懒标记 
} tree[N];

void pushup(int u) {
	tree[u].size = tree[tree[u].l].size + tree[tree[u].r].size + 1;
}

void pushdown(int u) { // 下传懒标记 
	swap(tree[u].l, tree[u].r);
	tree[u].tag = 0;
	tree[tree[u].l].tag ^= 1, tree[tree[u].r].tag ^= 1;
}

int newNode(int v) {
	++ idx;
	tree[idx].l = tree[idx].r = tree[idx].tag = 0;
	tree[idx].val = v, tree[idx].key = rand();
	tree[idx].size = 1;
	return idx;
}

void split(int p, int k, int &x, int &y) { // 按k(子树大小)分裂 
	if (!p) {x = y = 0; return ;}
	if (tree[p].tag) pushdown(p);
	
	if (tree[tree[p].l].size < k) {
		x = p;
		split(tree[p].r, k-tree[tree[p].l].size-1, tree[p].r, y);
	} else {
		y = p;
		split(tree[p].l, k, x, tree[p].l);
	}
	pushup(p);
}

int merge(int x, int y) {
	if (!x || !y) return x+y;
	if (tree[x].key <= tree[y].key) {
		if (tree[x].tag) pushdown(x);
		tree[x].r = merge(tree[x].r, y);
		pushup(x); return x;
	} else {
		if (tree[y].tag) pushdown(y);
		tree[y].l = merge(x, tree[y].l);
		pushup(y); return y;
	}
}

void insert(int v) {
	int x, y, z;
	split(root, v, x, z);
	y = newNode(v);
	root = merge(merge(x, y), z);
}

void reverse(int l, int r) { // 翻转区间[l,r] 
	int x, y, z;
	split(root, r, x, z); // 分裂区间[1,r],[r+1,n]
	split(x, l-1, x, y); // 分裂区间[1,l-1],[l,r]
	// 注意必须先分裂r再分裂l-1
	tree[y].tag ^= 1; // 打上懒标记
	root = merge(merge(x, y), z);
}

void print(int u) {
	if (!u) return ;
	if (tree[u].tag) pushdown(u);
	print(tree[u].l), printf("%d ", tree[u].val), print(tree[u].r); 
}

int main() {
	scanf("%d%d", &n, &m);
	for (int i = 1; i <= n; ++i) insert(i);
	
	int l, r;
	while (m -- ) {
		scanf("%d%d", &l, &r);
		reverse(l, r);
	}
	print(root);
	
	return 0;
}

6.11:

P2461 [SDOI2008] 递归数列:矩阵快速幂(紫)

\(S_i=\sum_{j=1}^i a_i\),则题目所求即为 \(S_n-S_{m-1}\)。考虑矩阵快速幂来计算 \(S_i\)

令:

\[\begin{aligned} A=\begin{bmatrix} 1 & 0 & 0 & \cdots & 0 \\ c_1 & c_1 & 1 & \cdots & 0\\ c_2 & c_2 & 0 & \cdots & 0\\ \vdots & \vdots & \vdots & \vdots & \vdots\\ c_{k-1} & c_{k-1} & 0 & \cdots & 1\\ c_k & c_k & 0 & \cdots & 0 \end{bmatrix} \end{aligned} \]

则有:

\[\begin{aligned} (S_i,a_i,a_{i-1},\cdots ,a_{i-k+1}) &= A(S_{i-1},a_{i-1},a_{i-2},\cdots,a_{i-k})\\ &=A^{i-k}(S_k,a_k,a_{k-1},\cdots a_1) \end{aligned} \]

注意 \(m,n\le k\) 时,需要直接返回答案。

时间复杂度 \(O(k^3\log n)\)

点击查看代码
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>

using namespace std;

#define int long long

const int N = 20;

int n, m, k, p, a[N][N], b[N], c[N], ans[N][N];

void mul(int a[][N], int b[][N], int c[][N]) {
	int tmp[N][N]; memset(tmp, 0, sizeof tmp);
	for (int i = 1; i < N; ++i) {
		for (int j = 1; j < N; ++j) {
			for (int k = 1; k < N; ++k)
				tmp[i][j] = (tmp[i][j] + a[i][k]*b[k][j]) % p;
		}
	}
	memcpy(c, tmp, sizeof tmp);
}

void power(int x) {
	while (x) {
		if (x & 1) mul(ans, a, ans);
		x >>= 1;
		mul(a, a, a);
	}
}

int calc(int x) {
	int sum = 0;
	if (x <= k) {
		for (int i = 1; i <= x; ++i) sum += b[i];
		return sum;
	}
	
	power(x-k);
	int t = ans[1][1];
	
	memset(a, 0, sizeof a), memset(ans, 0, sizeof ans);
	a[1][1] = 1, a[1][2] = 0;
	for (int i = 2; i <= k+1; ++i) a[i][1] = a[i][2] = c[i-1];
	for (int i = 2; i <= k; ++i) a[i][i+1] = 1;
	for (int i = 2; i <= k+1; ++i) ans[1][i] = b[k-i+2], ans[1][1] += ans[1][i];
	
	return t;
}

signed main() {
	ios::sync_with_stdio(0);
	cin.tie(0), cout.tie(0);
	
	cin >> k;
	for (int i = 1; i <= k; ++i) cin >> b[i];
	for (int i = 1; i <= k; ++i) cin >> c[i];
	cin >> m >> n >> p;
	
	a[1][1] = 1, a[1][2] = 0;
	for (int i = 2; i <= k+1; ++i) a[i][1] = a[i][2] = c[i-1];
	for (int i = 2; i <= k; ++i) a[i][i+1] = 1;
	for (int i = 2; i <= k+1; ++i) ans[1][i] = b[k-i+2], ans[1][1] += ans[1][i];
	
	cout << ((calc(n) - calc(m-1)) % p + p) % p << '\n';
	return 0;
}

/*
2
1 1
1 1
2 10 1000003
*/

P1306 斐波那契公约数:* 数学,矩阵快速幂(蓝)

\(f_i\) 表示斐波那契数列第 \(i\) 项。

引理 1\(\gcd(f_i,f_{i+1})=1\)

证明:(反证法)

若引理不成立,令 \(\gcd(f_i,f_{i+1})=a\;(a>1)\)

由于 \(f_{i-1}=f_{i+1}-f_i\),所以 \(f_{i-1}\) 也能被 \(a\) 整除。以此类推,可得 \(f_1=1\) 也能被 \(a\) 整除,矛盾。故引理成立。

引理 2\(f_n=f_{i+1}f_{n-i}+f_if_{n-i-1}\)

证明:(数学归纳法)

\(i=1\) 时,显然 \(f_n=f_2f_{n-1}+f_1f_{n-2}=f_{n-1}+f_{n-2}\) 成立。

假设 \(i=k\) 时,结论成立,有 \(f_n=f_{k+1}f_{n-k}+f_kf_{n-k-1}\)。则有:

\[\begin{aligned} f_n &= f_{k+1}f_{n-k}+f_kf_{n-k-1} \\ &= (f_k+f_{k+1})f_{n-k-1}+f_{k+1}f_{n-k-2}\\ &= f_{k+2}f_{n-k-1}+f_{k+1}f_{n-k-2} \end{aligned} \]

\(i=k+1\) 时,结论也成立。引理得证。

定理\(\gcd(f_n,f_m)=f_{\gcd(n,m)}\)

证明:

不妨设 \(n\ge m\)

根据引理 2,我们有 \(f_n=f_{n-m+1}f_m+f_{n-m}f_{m-1}\)。则 \(\gcd(f_n,f_m)=\gcd(f_{m},f_{n-m}f_{m-1})=\gcd(f_m,f_{n-m})\)。可以发现,这就是辗转相减法的过程,定理得证。


根据:

\[(f_i,f_{i-1})=\begin{bmatrix} 1 & 1\\ 1 & 0 \end{bmatrix}(f_{i-1},f_{i-2})=\begin{bmatrix} 1 & 1\\ 1 & 0 \end{bmatrix}^{i-2}(1,1) \]

可以直接用矩阵快速幂来求解。

时间复杂度 \(O(\log n)\)

点击查看代码
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>

using namespace std;

#define int long long

const int N = 5, p = 1e8;

int n, m, a[N][N], ans[N][N];

void mul(int a[][N], int b[][N], int c[][N]) {
	int tmp[N][N]; memset(tmp, 0, sizeof tmp);
	for (int i = 1; i < N; ++i) {
		for (int j = 1; j < N; ++j)
			for (int k = 1; k < N; ++k)
				tmp[i][j] = (tmp[i][j] + a[i][k] * b[k][j]) % p;
	}
	memcpy(c, tmp, sizeof tmp);
}

void power(int x) {
	ans[1][1] = ans[1][2] = 1;
	
	while (x) {
		if (x & 1) mul(ans, a, ans);
		x >>= 1;
		mul(a, a, a); 
	}
}

signed main() {
	cin >> n >> m;
	
	a[1][1] = a[1][2] = a[2][1] = 1;
	
	power(__gcd(n, m)-2);
	cout << ans[1][1] << '\n';
	return 0;
} 

补一道之前过了的题:

P2260 [清华集训2012]模积和:* 数论分块(紫)

不妨设 \(n\le m\)。则有:

\[\begin{aligned} &\sum_{i=1}^{n} \sum_{j=1}^{m} [i\ne j](n\bmod i)(m\bmod j)\\ =&\sum_{i=1}^n\sum_{j=1}^m(n\bmod i)(m\bmod j)-\sum_{i=1}^n(n\bmod i)(m\bmod i)\\ =&\sum_{i=1}^n\sum_{j=1}^m\left ( n-i\left \lfloor \dfrac{n}{i} \right \rfloor \right ) \left ( m-j\left \lfloor \dfrac{m}{j} \right \rfloor \right ) - \sum_{i=1}^n\left ( n-i\left \lfloor \dfrac{n}{i} \right \rfloor \right ) \left ( m-i\left \lfloor \dfrac{m}{i} \right \rfloor \right )\\ =&\left ( n^2-\sum_{i=1}^{n} i\left \lfloor \dfrac{n}{i} \right \rfloor \right )\left ( m^2-\sum_{i=1}^{m}i\left \lfloor \dfrac{m}{i} \right \rfloor \right )-\sum_{i=1}^{n}\left(nm-mi\left\lfloor \dfrac{n}{i}\right\rfloor-ni\left\lfloor \dfrac{m}{i}\right\rfloor+i^2\left\lfloor \dfrac{n}{i}\right\rfloor\left\lfloor \dfrac{m}{i}\right\rfloor\right) \end{aligned} \]

数论分块计算即可。时间复杂度 \(O(\sqrt n)\)

点击查看代码
#include <iostream>
#include <cstdio>
#include <algorithm>

using namespace std;

#define int long long
#define ll __int128

const int p = 19940417;

int n, m, ans;

int calc1(int k) { // 1+2+...+k
    return (ll)k * (k+1) / 2 % p;
}

int calc2(int k) { // 1^2+2^2+...+k^2
    return (ll)k * (k+1) * (2*k+1) / 6 % p;
}

signed main() {
    cin >> n >> m;
    if (n > m) swap(n, m);

    int ans1 = n*n % p;
    int l = 0, r = 0;
    while (l <= n) {
        l = r+1;
        if (n / l == 0) break;
        r = min(n, n/(n/l));
        ans1 = ((ans1 - (calc1(r)-calc1(l-1)+p)%p*(n/l)%p)%p + p) % p;
    }

    int ans2 = m*m % p;
    l = 0, r = 0;
    while (l <= m) {
        l = r+1;
        if (m / l == 0) break;
        r = min(m, m/(m/l));
        ans2 = ((ans2 - (calc1(r)-calc1(l-1)+p)%p*(m/l)%p)%p + p) % p;
    }

    int ans3 = 0;
    l = 0, r = 0;
    while (l <= n) {
        l = r+1;
        if (n / l == 0) break;
        r = min(n, min(m/(m/l), n/(n/l)));
        ans3 = ((ans3 + (ll)n*m%p*(r-l+1)%p - (ll)m*(calc1(r)-calc1(l-1)+p)%p*(n/l) 
                - (ll)n*((calc1(r)-calc1(l-1)+p)%p*(m/l)) + (ll)(calc2(r)-calc2(l-1)+p)*(m/l)*(n/l)) 
                % p + p) % p;
    }

    ans = ((ll)ans1 * ans2 % p - ans3 % p + p) % p;
    cout << ans << '\n';
    return 0;
}

6.14:

P2195 HXY造公园:* 并查集,树的直径(紫)

需要维护节点连通性,考虑使用并查集。对于每棵树的根节点 \(x\),记 \(d_x\) 表示 \(x\) 所在的树的直径。(求法参考:树上科技-树的直径

考虑如何维护这个森林:

对于操作 1,直接输出 \(d_{root(x)}\) 即可。对于操作 2,可以将 \(u,v\) 所在的树合并,合并后的树的最小直径为 \(\max\left\{\left\lceil \dfrac{d_{root(u)}}{2} \right\rceil+\left\lceil \dfrac{d_{root(v)}}{2} \right\rceil+1, d_{root(u)}, d_{root(v)}\right\}\)

时间复杂度 \(O(n\log n)\)

点击查看代码
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <vector>
#include <cmath>

using namespace std;

const int N = 3e5+10;

int n, m, q;
int p[N], dist[N], d[N]; // d[x]表示x所在树的直径(x为根节点) 
vector<int> e[N];

int find(int x) {
	if (p[x] != x) p[x] = find(p[x]);
	return p[x];
}

void merge(int u, int v, int op) {
	int pu = find(u), pv = find(v);
	if (pu == pv) return ;
	
	p[pu] = p[pv];
	if (op) d[pv] = max((int)(ceil(d[pu] / 2.0) + ceil(d[pv] / 2.0)) + 1, max(d[pu], d[pv]));
}

int dfs(int u, int fa) {
	int ans = u;
	for (auto v : e[u]) {
		if (v == fa) continue;
		dist[v] = dist[u] + 1;
		int x = dfs(v, u);
		if (dist[ans] < dist[x]) ans = x;
	}
	return ans;
}

int main() {
	scanf("%d%d%d", &n, &m, &q);
	for (int i = 1; i <= n; ++i) p[i] = i;
	
	int u, v;
	while (m -- ) {
		scanf("%d%d", &u, &v);
		merge(u, v, 0);
		e[u].push_back(v), e[v].push_back(u);
	}
	
	for (int i = 1; i <= n; ++i) {
		if (i != p[i]) continue;
		dist[i] = 0; int s = dfs(i, -1);
		dist[s] = 0; int t = dfs(s, -1);
		d[i] = dist[t];
	}
	
	int op;
	while (q -- ) {
		scanf("%d", &op);
		if (op == 1) scanf("%d", &u), printf("%d\n", d[find(u)]);
		else scanf("%d%d", &u, &v), merge(u, v, 1); 
	}
	
	return 0;
}

6.17:

P3938 斐波那契:二分,数学(绿)

可以发现,\(fa_i=i-\max_{1\le f_j<i} f_j\)。计算出 \(u,v\) 的所有祖宗节点后二分查找即可。

点击查看代码
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <map>
#include <cmath>
#include <vector>

using namespace std;

#define int long long

const int N = 1000, M = 3e5+20;

int m;
int f[N] = {1, 1, 2};

void get(int x, vector<int> &X) {
	int t = lower_bound(f+1, f+60, x) - f;
	X.push_back(x);
	while (x > 1 && t > 0) {
		if (x-f[t] >= 1) x -= f[t], X.push_back(x);
		t --; 
	}
	sort(X.begin(), X.end());
}

signed main() {
	for (int i = 3; i <= 65; ++i) f[i] = f[i-1] + f[i-2];
	
	scanf("%lld", &m);
	int x, y;
	for (int i = 1; i <= m; ++i) {
		scanf("%lld%lld", &x, &y);
		vector<int> X, Y;
		get(x, X), get(y, Y);
		
		int l = 0, r = min(X.size(), Y.size()) - 1;
		while (l < r) {
			int mid = l + r + 1 >> 1;
			if (X[mid] == Y[mid]) l = mid;
			else r = mid-1;
		}
		printf("%lld\n", X[l]);
	}
	return 0;
}

6.19:

P4281 [AHOI2008]紧急集合 / 聚会:LCA(蓝)

先考虑 \(2\) 个点 \(a,b\) 的情况,显然此时 \(p=\text{lca}(a,b)\)

再加入第 \(3\) 个点 \(c\),可以猜测,\(p\) 一定为 \(\text{lca}(a,b),\text{lca}(a,c),\text{lca}(b, c)\) 中的一个点。

证明:

对于一条树上路径 \(a\rightarrow b\),其中的每一个点 \(p\) 都满足 \(d(a,p)+d(b,p)=d(a,b)\),所以我们应尽可能使 \(p\) 靠近 \(c\) 点。分类讨论:

  1. \(c\)\(a\rightarrow b\) 路径上:\(p=c\)
  2. \(c\) 不在 \(a\rightarrow b\) 路径上:
  • \(c\) 在以 \(a\) 为根的子树中:\(p=a\)
  • \(c\) 在以 \(b\) 为根的子树中:\(p=b\)
  • \(c\) 不在 以 \(a,b\) 为根的子树中:\(p=\text{lca}(a,b)\)(因为 \(\text{lca}(a,b)\)\(a\rightarrow b\) 路径中深度最小的节点)。

\(a,b,c\) 地位等同,原命题得证。

那么分别计算 \(\text{lca}(a,b),\text{lca}(a,c),\text{lca}(b, c)\)\(a,b,c\) 距离之和的最小值即可。

时间复杂度 \(O(n\log n+m)\)

点击查看代码
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <vector>
#include <cmath>

using namespace std;

const int N = 5e5+10;

int n, m;
int idx, dep[N], dfn[N<<1], pos[N];
int st[N<<1][20];
vector<int> e[N];

void dfs(int u, int fa) {
	dfn[++ idx] = u, pos[u] = idx;
	for (auto v : e[u]) {
		if (v == fa) continue;
		dep[v] = dep[u]+1, dfs(v, u);
		dfn[++ idx] = u;
	}
}

int Min(int a, int b) {
	return (pos[a] < pos[b]) ? a : b;
}

void init() {
	for (int i = 1; i <= idx; ++i) st[i][0] = dfn[i];
	for (int i = 1; (1<<i) <= idx; ++i) {
		for (int j = 1; j <= idx-(1<<i)+1; ++j)
			st[j][i] = Min(st[j][i-1], st[j+(1<<i-1)][i-1]);
	}
}

int query(int l, int r) {
	if (l > r) swap(l, r);
	int k = log2(r-l+1);
	return Min(st[l][k], st[r-(1<<k)+1][k]);
}

int lca(int u, int v) {
	u = pos[u], v = pos[v];
	return query(u, v);
}

int dist(int u, int v) {
	return dep[u] + dep[v] - 2 * dep[lca(u, v)];
}

void solve(int a, int b, int c) {
	int p1 = lca(a, b), p2 = lca(a, c), p3 = lca(b, c);
	int dis1 = dist(a, p1)+dist(b, p1)+dist(c, p1), dis2 = dist(a, p2)+dist(b, p2)+dist(c, p2), 
		dis3 = dist(a, p3)+dist(b, p3)+dist(c, p3);
	int t = min(dis1, min(dis2, dis3));
	if (t == dis1) printf("%d %d\n", p1, dis1);
	else if (t == dis2) printf("%d %d\n", p2, dis2);
	else printf("%d %d\n", p3, dis3);
} 

int main() {
	scanf("%d%d", &n, &m);
	int u, v;
	for (int i = 1; i < n; ++i) {
		scanf("%d%d", &u, &v);
		e[u].push_back(v), e[v].push_back(u);
	}
	
	dfs(1, -1);
	init();
	
	int a, b, c;
	while (m -- ) {
		scanf("%d%d%d", &a, &b, &c);
		solve(a, b, c);
	}
	return 0;
}

P6216 回文匹配:(* 字符串究极神题)manacher,KMP,前缀和(蓝)

裸体就意味着身体。

首先对 \(S\)\(T\) 做一次 KMP,若 \(S[l\cdots r]=T[1\cdots m]\),则令 \(a_l=1\)。然后再在 \(S\) 上跑一遍 manacher,求出以 \(S_i\) 为中心的最长回文子串。

对于 \(S_i\),考虑其对答案的贡献。则 \(S[i-d_i+1\cdots i+d_i-1]\) 为以 \(S_i\) 为中心的最长回文子串。令 \(l=i-d_i+1,r=i+d_i-m\)(保证 \(T\) 右端点在回文子串内),那么 \(S_i\) 对答案的贡献即为:

\[\begin{aligned} & (a_l+a_{l+1}+\cdots +a_r) + (a_{l+1}+a_{l+2}+\cdots +a_{r-1}) + \cdots \\ = & \sum_{l\le j\le mid} a_j(j-l+1) + \sum_{mid<j\le r} a_j(r-j+1) \\ = & -(l-1)\sum_{l\le j\le mid}a_j+\sum_{l\le j\le mid}j\cdot a_j+(r+1)\sum_{mid<j\le r} a_j-\sum_{mid<j\le r}j\cdot a_j \end{aligned} \]

(其中 \(mid=\left\lfloor \dfrac{l+r}{2} \right\rfloor\)

注意到 \(\sum a_j\)\(\sum j\cdot a_j\) 可以用前缀和预处理。

时间复杂度 \(O(n+m)\)

点击查看代码
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <vector>

using namespace std;

const int N = 3e6+10;

int n, m, ne[N], d[N];
unsigned int a[N], s1[N], s2[N];
unsigned int ans;
char S[N], T[N];

void get_next() {
	ne[1] = 0;
	for (int i = 2, j = 0; i <= m; ++i) {
		while (T[i] != T[j+1] && j) j = ne[j];
		if (T[i] == T[j+1]) j ++;
		ne[i] = j;
	}
}

void get_d() {
	d[1] = 1;
	for (int i = 2, l = 0, r = 0; i <= n; ++i) {
		int j = l + r - i;
		if (i <= r) d[i] = min(d[j], r-i+1);
		while (S[i+d[i]] == S[i-d[i]] && i-d[i] >= 1 && i+d[i] <= n) d[i] ++;
		if (i+d[i] > r) r = i+d[i]-1, l = i-d[i]+1;
	}
}

int main() {
	ios::sync_with_stdio(0);
	cin.tie(0), cout.tie(0);
	
	cin >> n >> m >> S+1 >> T+1;
	
	get_next();
	
	for (int i = 1, j = 0; i <= n; ++i) {
		while (S[i] != T[j+1] && j) j = ne[j];
		if (S[i] == T[j+1]) j ++;
		if (j == m) a[i-m+1] ++, j = ne[j];
	}
	for (int i = 1; i <= n; ++i) s1[i] = s1[i-1]+a[i], s2[i] = s2[i-1]+i*a[i];
	
	get_d();
	
	for (int i = 1; i <= n; ++i) {
		int l = i-d[i]+1, r = i+d[i]-m, mid = l + r >> 1;
		if (l > r) continue;
		ans += -(unsigned int)(l-1)*(s1[mid]-s1[l-1]) + s2[mid]-s2[l-1];
		if (r != mid) ans += (unsigned int)(r+1)*(s1[r]-s1[mid]) - s2[r]+s2[mid];
	}
	
	cout << ans << '\n';
	return 0;	
}

Codeforces Round 878 (Div. 3):

CF1840A

CF1840B\(k\) 位二进制数能表示的最大数为 \(2^{k}-1\)。当 \(n>2^k-1\) 时,\(ans=2^k\);否则 \(ans=n+1\)。时间复杂度 \(O(t)\)

CF1840C:预处理 \(a\) 中所有 \(>q\) 的数的位置,扔到一个 vector 中。正序扫描 \(a\),对于 \(a_i\),二分查找其后第一个 \(>q\) 的位置 \(pos\),若 \(pos-i+1\ge k\)\(a_i\) 对答案的贡献为 \(pos-i+1-k\)

CF1840D:二分答案即可。

点击查看代码
#include <iostream>
#include <cstdio>
#include <algorithm>

using namespace std;

const int N = 2e5+10;

int t, n, a[N];

bool check(int num) {
	int x = 1, cnt = 0;
//	printf("%d\n", num);
	while (x <= n && cnt < 3) {
//		printf("%d ", x);
		x = upper_bound(a+1, a+n+2, a[x]+2*num) - a, cnt ++;
	}
//	printf("%d\n", x);
	if (x > n) return 1;
	return 0;
}

int main() {
	scanf("%d", &t);
	while (t -- ) {
		scanf("%d", &n);
		for (int i = 1; i <= n; ++i) scanf("%d", &a[i]);
		a[n+1] = 1000000000;
		
		sort(a+1, a+n+2);
		
		int l = 0, r = 1000000000;
		while (l < r) {
			int mid = l + r >> 1;
			if (check(mid)) r = mid;
			else l = mid+1;
		}
		printf("%d\n", l);
	}
	return 0;
}

CF1840E:用一个变量 \(cnt\) 记录当前 \({a}_i\ne {b}_i\) 的字符数量,\(O(1)\) 更新即可。(虽然但是我比赛时脑抽用了树状数组 www)

点击查看代码
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <vector>

using namespace std;

typedef pair<int, int> pii;

const int N = 2e5+10;

int T, t, q, n, c[N];
string s1, s2;
char s[3][N];
int id; vector<pii> oper; 

int lowbit(int x) {
	return x & -x;
}

void add(int x, int k) {
	for (int i = x; i <= n; i += lowbit(i))
		c[i] += k;
}

int query(int x) {
	int sum = 0;
	for (int i = x; i > 0; i -= lowbit(i))
		sum += c[i];
	return sum;
}

int main() {
	ios::sync_with_stdio(0);
	cin.tie(0), cout.tie(0);
	
	cin >> T;
	while (T -- ) {
		id = 0, oper.clear(); memset(c, 0, sizeof c);
		
		cin >> s1 >> s2 >> t >> q;
		
		n = s1.size();
		for (int i = 1; i <= n; ++i) s[0][i] = s1[i-1], s[1][i] = s2[i-1];
		
		for (int i = 1; i <= n; ++i) {
			if (s[0][i] != s[1][i]) 
				add(i, 1);
		}
				
		int op, x, y, pos1, pos2;
		for (int i = 1; i <= q; ++i) {
			if (id < oper.size() && i == oper[id].first) {
				add(oper[id].second, 1);
				id ++;
			}
						
			cin >> op;
			if (op == 1) {
				cin >> pos1;
				if (s[0][pos1] != s[1][pos1]) add(pos1, -1), oper.push_back({i+t, pos1});
			} else if (op == 2) {
				cin >> x >> pos1 >> y >> pos2; x --, y --;
				if (s[x][pos1] != s[x^1][pos1]) add(pos1, -1);
				if (s[y][pos2] != s[y^1][pos2]) add(pos2, -1);
				swap(s[x][pos1], s[y][pos2]);
				if (s[x][pos1] != s[x^1][pos1]) add(pos1, 1);
				if (s[y][pos2] != s[y^1][pos2]) add(pos2, 1);
			} else if (op == 3) {
				if (!query(n)) cout << "YES\n";
				else cout << "NO\n";
			}
			
//			for (int i = 1; i <= n; ++i) {
////				if (query(i)-query(i-1) == 0)
//					cout << s[0][i];
//			}
//			cout << '\n';
//			for (int i = 1; i <= n; ++i) {
////				if (query(i)-query(i-1) == 0)
//					cout << s[1][i];
//			}
//			cout << '\n';
//			for (int i = 1; i <= n; ++i) cout << query(i) - query(i-1) << ' ';
//			cout << '\n';
		}
	}
	
	return 0;
}
posted @ 2023-07-05 21:56  Jasper08  阅读(28)  评论(0)    收藏  举报