「数据结构」杂谈
引子
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) 的情况:
- x 与 y 不在同一棵树上,这样我们只需要正常连边就可以。
- x 与 y 已经在一个联通分量里了,这样我们什么操作都不需要做。
- x 与 y 在一棵树上,这样的话 x 到 y 的路径上的点会成为一个新的边双联通分量,我们需要把这些点缩成一个点。
具体维护方式我们利用 LCT 和 并查集 来进行维护,我们利用并查集来实现第三个操作,我们每次把 x 到 y 的路径上的点 split 出来,直接 dfs 遍历 splay,把这些点的并查集上的父亲设成同一个点就可以了。
这个题我好像也没有亲自写过,不过写过一道类似的题目。
一道例题
题目大意
对于一棵 BST 而言,我们定义查找一个值的代价为 : 查询过程中, 经过的所有节点上的值之和。维护 \(n\) 棵 BST, 初始为空, 两种操作 :
- 将第 \([l, r]\) 的 BST 中,全部插入一个值 \(w\);
- 求出在第 \(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\)。
题目解析
其实大部分题我都没写过,甚至题解也没有,只是凭借听课时的记忆和自己的理解写出来的。
这道题我们可以分成两个部分:
- 求出每个人可以带的物品种类。
- 最大化满足题意的物品数量。
我们先用数据结构来解决第一个问题,显然我们可以轻重链剖分,然后用一个 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 两种方法。
- 找重心:
一棵树最多只会有两个重心,所以我们可以把树根定为树的重心,这样就可以判断了。 - 换根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;
}