zoukankan      html  css  js  c++  java
  • 「数据结构」杂谈

    引子

    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 << '
    '; 
    	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] << '
    '; 
    	}
    	return 0; 
    }
    
  • 相关阅读:
    我和计算机
    十四周学习记录
    十五周个人作业
    怎样成为一个高手
    C语言第0次作业
    C语言博客作业03函数
    C博客作业01分支、顺序结构
    C语言博客作业02循环结构
    Rails后台,Firefox Addons前端,JS的AJAX调用
    Ruby 三元一次线性方程组
  • 原文地址:https://www.cnblogs.com/longdie/p/14510101.html
Copyright © 2011-2022 走看看