「数据结构」杂谈

引子

diamond_duke 不知道是哪位国家队神犇,% 就完了,不过课讲的我听的不是很懂。

热身小练习

平方串

\(O(nlogn)\) 求所有平方串,用后缀数组预处理 lcp,然后枚举长度 \(L\),会将整个串分成若干段,然后相邻端点之间求 lcp,如果长度大于等于 \(L\),那么这是一个平方串。

复杂度枚举单次长度是 \(O(n)\) 的,因为这是一个调和级数,所以复杂度大概是 \(O(nlogn)\)

[SCOI2016]萌萌哒

题目链接

题目大意

给一个仅有 \('0'-'9'\) 组成的字符串,并且给出一些其平方串(即两个长度相同的串相同)的长度和位置,求出合法的串的方案数。

题目解析

不难想到 \(O(n^2logn)\)并查集,实际上这道题就是并查集的一个应用。

我们发现我们需要 \(O(n^2)\) 去做并查集的预处理,然后用 \(O(n)\) 的复杂度去计算答案,我们可以通过调整使其复杂度均摊到 \(O(nlogn)\)

我们可以把并查集的节点个数开到 \(nlogn\) 个,\(f[i][j]\) 表示以 \(i\) 为左端点,\(i+(1<<j)-1\) 为右端点的区间与另一个区间完全相同。

然后我们对于一个区间就可以用小于 \(logn\) 次去合并,然后最后用类似 ST 表的思想去把所有的信息从上到下迭代到最后一层,然后去统计。

题目代码
代码
// by longdie 
#include <bits/stdc++.h> 
using namespace std; 
const int N = 1e5 + 5, mod = 1e9 + 7; 
int n, m, f[N][18], ans; 
inline int find(int i, int j) { return f[i][j] == i ? i : f[i][j] = find(f[i][j], j); } 
inline void merge(int i, int j, int k) {
	f[i][k] = find(i, k), f[j][k] = find(j, k); 
	f[f[i][k]][k] = f[j][k]; 
}
inline int qpow(int a, int b, int res = 1) {
	for(; b; b >>= 1, a = 1ll*a*a%mod) 
		if(b & 1) res = 1ll*res*a % mod; 
	return res; 
}
signed main() {
	scanf("%d%d", &n, &m); 
	for(register int i = 1; i <= n; ++i) 
		for(register int j = 0; j < 18; ++j) f[i][j] = i; 
	for(register int i = 1; i <= m; ++i) {
		int l0, r0, l1, r1; scanf("%d%d%d%d", &l0, &r0, &l1, &r1); 
		for(register int j = 17, t0=l0, t1=l1; j >= 0; --j) {
			if(r0-t0+1 < 1 << j) continue; 
			merge(t0, t1, j), t0 += 1 << j, t1 += 1 << j; 
		}
	}
	for(register int j = 17; j; --j) {
		for(register int i = 0; (i + (1<<j) - 1) <= n; ++i) {
			int fa = find(i, j);
			merge(i, fa, j-1), merge(i + (1<<j-1), fa + (1<<j-1), j-1); 
		}
	}
	for(register int i = 1; i <= n; ++i) if(find(i, 0) == i) ans++; 
	ans = 9ll * qpow(10, ans - 1) % mod; 
	cout << ans << '\n'; 
	return 0; 
}

本题题意

给定序列 \(S = a_1 , a_2 , · · · , a_n\) ,建立一张 \(n\) 个点的空图。对于平方串 \(S[i : i + L − 1] = S[i + L : i + 2L − 1]\) , 我们会给图中 \(i + j − 1\)
\(i + j + L − 1\) 连一条边 , 边权为 w_L ,其中 \(j = 1, 2, · · · , L\)。求该图的最小生成树。
\(n ≤ 3 × 10^5 ,1 ≤ w_i ≤ 10^9\)

本题解法

因为求的是 MST,所以我们可以利用 Kruscal 的思想,按照 \(w_L\) 从小到大排序,然后依次考虑,如果可以连边,那就直接连,然后利用 萌萌哒 那道题的思想,把点拆成 \(nlogn\) 个,最后合并即可。

我只是个云玩家,自己没有写过,但是思路大体是这样的。

[ZJOI2016]大森林

题目链接

这道题我现在也没写过,只是大概说一下我理解的思路和做法。

显然这道题是关于 LCT 的吧,然后我们每次也只能维护一棵树,所以我们考虑离线解决问题。

即每次从第 \(i\) 棵树转移到第 \(i+1\) 棵树,相当于在找不同,然后作出相应的修改。

首先我们需要实现 lca 操作:对于 \(x,y\) 两个点,我们可以先 access(x),然后 access(y),记录在 access 过程中最后一次需要改变实儿子的位置,然后这个位置就是它们的 lca 。

然后就是实现 1 操作,并且满足快速转移的条件,我们可以对于每个 1 操作,新建一个虚点,把所有需要连向它的边全连在它的虚点上,这样只需要一次 cut,link 操作就可以实现快速转移,复杂度也就是 \(O(nlogn)\) 了。

[bzoj 2959] 长跑

题目链接

这道题的简要题意就是维护边双联通分量

我们采用 LCT 来实现维护,具体的因为只有加边操作,没有删边操作,我们讨论加入一条边(x,y) 的情况:

  1. x 与 y 不在同一棵树上,这样我们只需要正常连边就可以。
  2. x 与 y 已经在一个联通分量里了,这样我们什么操作都不需要做。
  3. x 与 y 在一棵树上,这样的话 x 到 y 的路径上的点会成为一个新的边双联通分量,我们需要把这些点缩成一个点。

具体维护方式我们利用 LCT 和 并查集 来进行维护,我们利用并查集来实现第三个操作,我们每次把 x 到 y 的路径上的点 split 出来,直接 dfs 遍历 splay,把这些点的并查集上的父亲设成同一个点就可以了。

这个题我好像也没有亲自写过,不过写过一道类似的题目。

一道例题

题目大意

对于一棵 BST 而言,我们定义查找一个值的代价为 : 查询过程中, 经过的所有节点上的值之和。维护 \(n\) 棵 BST, 初始为空, 两种操作 :

  1. 将第 \([l, r]\) 的 BST 中,全部插入一个值 \(w\);
  2. 求出在第 \(x\) 棵 BST 中,查询 \(w\) 的代价。

\(n, q ≤ 2 × 10^5 ,1 ≤ w ≤ 10^9\) ,插入的值两两不同。

题目解析

主要解法是线段树维护单调栈。
我还不是很会,咕了。

「九省联考 2018」IIIDX

题目链接

题目大意

给出长度为 \(n\) 的序列 \(d_1 , d_2 , · · · , d_n\) ,重新排列使得 \(d_i ≥ d_{i/k}\) ,最大化得到序列的字典序。
\(n ≤ 5 × 10^5\) , \(k\) 给定。

题目解析

云玩家,没有真正写过。

显然我们可以建一棵树出来,且必须满足条件 u 节点的权值要大于等于它子树的权值。

首先我们考虑一个错误的贪心(在元素不重复的情况下是对的),把输入数据从大到小排序,然后显然我们需要预留子树个数个节点使其满足条件,我们按照子树节点编号优先选择就可以了,然后不断递归下去,这就是贪心的思路。

在有重复元素的情况下它是错误的,应该比较好理解,那么我们还是从大到小排序,去重后设 \(num[i]\) 表示 \(i\) 元素的出现次数,设 \(sum[i]\) 表示大于等于 \(i\) 的个数和(即前缀和),然后我们用线段树维护这个东西。

我们依旧用贪心的思路去解决问题,不过对于当前的一个节点,我们在线段树上二分找到它所能赋的最大值,然后减去这么多个(相当于预留),继续递归维护就可以了。

当然我们需要注意当遍历到一个节点 \(u\) 时,需要把它的父亲节点所提前减去的预留的贡献加回来。

另一道例题

题目大意

给定一棵有根树,边只能从下向上走,第 \(i\) 个节点会产出第 \(a_i\) 种物品。\(q\) 次询问,每次给出 c 个点(每个点有一个人),所有人会从各自的位置出发走到这些点的LCA,可以带走路过的点产出的物品。要求每个人带的物品数目一样,且所有人带的物品中没有重复的。最大化带的总物品个数。
\(n ≤ 3 × 10^5 ,q ≤ 5 × 10^4 ,c ≤ 5\),特产种类 \(≤ 1000\)

题目解析

其实大部分题我都没写过,甚至题解也没有,只是凭借听课时的记忆和自己的理解写出来的。

这道题我们可以分成两个部分:

  1. 求出每个人可以带的物品种类。
  2. 最大化满足题意的物品数量。

我们先用数据结构来解决第一个问题,显然我们可以轻重链剖分,然后用一个 bitset 来求出每个人可以带的物品种类。

具体的来讲就是轻链暴力跳,容易发现我们经过的重链只有最后一个不一定是完整的,所以我们可以对于每个重链顶端维护一个bitset,这样就可以把复杂度优化到 \(O(nlogn * m/32)\)

然后现在我们解决第二个问题,我们不难想到可以利用二分图最大匹配来解决问题,我们确实可以通过跑二分图最大匹配来解决问题,不过这样复杂度有点高。
所以我们可以利用 Hall定理来解决问题。

Hall定理:
Hall定理是判断一个二分图是否存在完美匹配的东西。
我们对于左部点的一个集合 \(S\) ,它所能连到的集合为 \(T\),如果对于任何一个集合都满足 \(|S| <= |T|\),那么二分图存在完美匹配。
一个推论:一个二分图的最大匹配数等于:\(|S| - max(|S'| - |T'|)\)

那么对于本题我们没有必要全部枚举,其实只用 \(2^c\) 枚举集合 \(S\),然后去取一个最小值就是答案了。

又一道例题

简略题意

给定 \(n\) 个节点的树,将每个节点染上 \([1, m]\) 之间的颜色,求使得所有同色点对距离的最小值介于 \([L, R]\) 之间的方案数。
\(n ≤ 10^5 ,m ≤ 10^9 ,1 ≤ L ≤ R < n\)

题目解析

这个题我暂时不会,说一下我理解的思路吧。
首先答案可以转化为至少为\(L\)的减去至少为\(R+1\)的,然后需要用到BFS序的一些性质,然后就没有然后了。

HDU 6368 Variance-MST

给定一个图,求最小方差生成树。(要求 \(O(nlogn)\) 的复杂度实现)

对于这个题,显然我们需要转化柿子吧。
后面我只知道这道题需要 LCT 来维护,后面就没有后面了。

区间查询 mex

题目链接

这道题在线的话可以做到时间 \(nlogn\),空间 \(nlogn\)
具体的经典做法是主席树维护,这个就不多说了。

然后如果离线的话可以做到时间 \(nlogn\),空间 \(O(n)\)
具体做法是我们把询问按照右端点排序,权值线段树上维护每个值出现的最右边的位置,然后就可以了。

树同构 & 树Hash

树同构应该很好理解,树Hash的主要作用是判断两棵有根树是否同构,当然通过一些改变也可以判断无向图的树同构。

计算方法

我们定义 \(ha[i]\) 表示 \(i\) 这棵子树的 Hash 值,那么 \(ha[u] = base + \sum_{v}^{son[u]}ha[v] * pri[siz[v]]\)
其中 \(pri[i]\) 为第 \(i\) 个质数。

无向图的计算方法

一般有找重心 和 换根DP 两种方法。

  1. 找重心:
    一棵树最多只会有两个重心,所以我们可以把树根定为树的重心,这样就可以判断了。
  2. 换根DP:
    我们可以通过DP求出每个点做根时的树Hash值,直接放到 map 里去匹配就可以了。
    DP 转移方程(v是u的一个儿子,且根从u转到v): \(ha[v] = ha[v] + (ha[u]-ha[v]*pri[siz[v]])*pri[n-siz[v]]\)

板子题链接

代码
// by longdie 
#include <bits/stdc++.h> 
#define ull unsigned long long 
using namespace std; 
const int N = 105; 
const ull base = 233; 
map<ull, int> p; 
ull ha[N]; 
int n, head[N], cnt=1, siz[N], rt1, rt2, Max, vis[N*20], tot, pri[N];  
struct edge { int to, next; } e[N<<1]; 
inline void add(int x, int y) { e[++cnt] = (edge){y, head[x]}, head[x] = cnt; } 
void dfs0(int u, int fa) {
	siz[u] = 1; 
	int res = 0; 
	for(register int i = head[u], v; i; i = e[i].next) {
		v = e[i].to; 
		if(v == fa) continue; 
		dfs0(v, u); 
		siz[u] += siz[v]; 
		res = max(res, siz[v]); 
	}
	res = max(res, n - siz[u]); 
	if(res < Max) Max = res, rt1 = u, rt2 = 0; 
	else if(res == Max) rt2 = u; 
}
void dfs(int u, int fa) {
	ha[u] = base, siz[u] = 1; 
	for(register int i = head[u], v; i; i = e[i].next) {
		v = e[i].to; 
		if(v == fa) continue; 
		dfs(v, u); 
		siz[u] += siz[v]; 
		ha[u] = ha[u] + ha[v]*pri[siz[v]]; 
	} 
}
signed main() {
	int T; scanf("%d", &T); 
	for(register int i = 2; tot <= 100; ++i) {
		if(!vis[i]) pri[++tot] = i; 
		for(register int j = 1; j <= tot && pri[j]*i <= 2000; ++j) {
			vis[i*pri[j]] = 1; 
			if(i % pri[j] == 0) break; 
		} 
	}
	for(register int t = 1; t <= T; ++t) {
		scanf("%d", &n); 
		cnt=1, memset(head, 0, sizeof(head)); 
		for(register int i = 1, x; i <= n; ++i) {
			scanf("%d", &x); 
			if(x) add(x, i), add(i, x); 
		}
		Max = n, dfs0(1, 0);
		dfs(rt1, 0); 
		ull res = ha[rt1]; 
		if(rt2) dfs(rt2, 0), res = min(res, ha[rt2]); 
		if(p.find(res) == p.end()) p[res] = t; 
		cout << p[res] << '\n'; 
	}
	return 0; 
}
posted @ 2021-03-10 19:58  longdie  阅读(122)  评论(0编辑  收藏  举报