一些比较基础的知识点

版权声明:蒟蒻写的文章,能看就行了,同时欢迎大佬们指点错误 https://blog.csdn.net/Algor_pro_king_John/article/details/83861926

树链剖分

  • 这是一个很简单的知识点.

  • 主要思想是把树按照重链进行剖分.

  • 重链指的是一条由顶点与重儿子相连,再由重儿子与重儿子的重儿子相连……组成的链。

  • 重儿子则指的是一个点所有儿子中size最大的节点.

  • 有了这个工具,我们再求解许多问题时都可以方便许多.

  • 例如,题目要求两点LCA,那么只需要这样打就好了:

int Go(int x, int y) {
	while (x ^ y) {
		if (top[x] == top[y]) return dep[x] > dep[y] ? y : x;
		if (dep[top[x]] < dep[top[y]]) swap(x, y);
		x = fa[top[x]];
	}
	return x;
}
  • 唯一需要注意的就是跳重链时要选择跳完后深度更低的那个点跳。

  • 预处理我们也可以很快的求出来,这样打:

void Dfs(I k) {
	E[++ E[0]] = k, sz[k] ++;
	for (I x = las[k], Hv = 0; x ; x = nex[x])
		if (!dep[tov[x]]) {
			fa[tov[x]] = k, dep[tov[x]] = dep[k] + 1;
			Dfs(tov[x]);
			Son[k] = sz[tov[x]] > Hv ? tov[x] : Son[k], sz[k] += sz[tov[x]], mx(Hv, sz[tov[x]]);
		}
}

F(i, 1, E[0])
		top[E[i]] = Son[fa[E[i]]] == E[i] ? top[fa[E[i]]] : E[i];
  • 套链剖的题太多了,这里就不讲了.

Huffman树

  • 给出一颗包含 n n 个节点的** k k 叉树**,其中第 i i 个叶子节点带权 w i w_i ,要求最小化 w i l i \sum w_i*l_i ,其中 l i l_i 表示第 i i 个叶子结点到根节点的距离。该问题的被称为 k k Huffman

  • 其实很早以前就已经接触过这个了,当时做过很经典的一道题:JZOJ4210

Problem
  • 给定一个非严格递减序列 A A B i = j = i n A j B_i=\sum_{j=i}^nA_j .

  • 现在有一个 n n n*n 网络,左下角 ( 1 , 1 ) (1,1) ,右上角 ( n , n ) (n,n) ,要求从 ( n , 1 ) ( 1 , 1 ) (n,1)\rightarrow(1,1) .

  • 走格方式: ( x , y ) ( x 1 , y + 1 ) (x,y)\rightarrow(x-1,y+1) ( x , y + 1 2 ) (x,\lceil \frac{y+1}{2} \rceil) ,其中后者要支付 B x B_x 代价.

  • 求最小代价。

Solution
  • 可以感受到这道题的 y + 1 2 \frac{y+1}{2} 很类似二叉树上的某种操作,我们尝试着进行类比.

  • 考虑一个非严格递减的序列 A A ,它的Huffman树应该如何构造。

  • 我们令f[i][j]表示当前Huffman树构造到序列的第 i i 个位置,即序列的前 i 1 i-1 个位置已经放好,现在准备放 A i A_i Huffman树上, j j 个叶子节点可以供放 A i A_i 这个数.

  • 注意前提 A A 是降序的, 所以不难发现,每个点对应在Huffman树上的深度必定是升序的.

  • 所以不难写出转移:

f [ i ] [ j ] + k = i n a k f [ i ] [ 2 j ] f[i][j] + \sum_{k=i}^n a_k \rightarrow f[i][2*j] f [ i ] [ j ] f [ i + 1 ] [ j 1 ] f[i][j] \rightarrow f[i +1][j - 1]

  • 含义分别是,把当前Huffman树上所有叶节点都扩展两个新叶节点,拿来供给以后 i n i\sim n 的节点放,但因为深度多了 1 1 ,所以不管怎样,以后的节点都至少要多一个 a k a_k 贡献,所以相当于加上一个 k = i n a k \sum_{k=i}^n a_k ,另外一个转移则是直接把当前 i i 号节点放在 j j 个叶节点的某一节点中.

  • 整个流程如下:

#include <cstdio>
#include <cstring>

#define F(i, a, b) for (int i = a; i <= b; i ++)
#define G(i, a, b) for (int i = a; i >= b; i --)
#define min(a, b) ((a) < (b) ? (a) : (b))
#define mem(a, b) memset(a, b, sizeof a)

const int N = 1e3 + 10;

int n, m, T, Ans;
int a[N], S[N], f[N][N];

int main() {
	scanf("%d", &n);
	F(i, 1, n) scanf("%d", &a[i]); S[n + 1] = 0;
	G(i, n, 1) S[i] = S[i + 1] + a[i];

	mem(f, 7), f[1][1] = 0;
	F(i, 1, n)
		F(j, 1, n)
			f[i + 1][j - 1] = min(f[i + 1][j - 1], f[i][j]),
			f[i][j * 2] = min(f[i][j * 2], f[i][j] + S[i]);

	Ans = 1e9;
	F(i, 1, n) Ans = min(Ans, f[n][i]);
	printf("%d\n", Ans);
}
  • 此时再观察一下题目,不难发现,这TM不正是题目的逆问题吗?

  • 考虑原问题的一个最优解,把其路径反过来,则正是DP的过程.

  • 观察到题目是上取整,也正好符合构建Huffman树和DP的过程.

  • 所以原问题相当于对其Huffman树解的询问,这个可以在 O ( n l o g n ) O(nlogn) 时间内解决.

  • 代码如下:

#include <cstdio>
#include <set>

#define F(i, a, b) for (int i = a; i <= b; i ++)

using namespace std;

const int N = 1e3 + 10;

int n, T, x; long long Ans;
multiset <int> S;

int main() {
for (scanf("%d", &T) ; T --; ) {

	scanf("%d", &n), S.clear(), Ans = 0;
	F(i, 1, n) scanf("%d", &x), S.insert(x);
	F(i, 1, n - 1) {
		int x = *S.begin(); S.erase(S.begin());
		int y = *S.begin(); S.erase(S.begin());
		Ans += (long long)(x + y), S.insert(x + y);
	}
	printf("%lld\n", Ans);
}

}
  • 回到Huffman树的原问题中,上述讨论的实质上是二叉Huffman树.

  • 如果讨论 k k 叉树,方法也是类似的,唯一需要注意的是必须通过在序列里补 0 0 的方式把 n 1 n-1 变为 k 1 k-1 的倍数.

  • 这是由于,我们必须让每次在set里选 k k 个时,不能选满的一次,必须发生在Huffman树的最底层.

  • 至此,有关哈夫曼树的问题暂且告一段落.

KMP

  • 即模式匹配,能够在线性时间内求出字符串 S S 在字符串 T T 中所有出现过的位置.

  • 其核心是 n e x t next 数组

  • n e x t [ i ] next[i] 表示 S [ 1 n e x t [ i ] ] = S [ n n e x t [ i ] + 1 n ] S[1\sim next[i]] = S[n-next[i]+1\sim n] .

  • 考虑普通的匹配,我们总是拿 T T 串当中的某一位置与 S S 串某一位置进行比较,不妨设当前枚举到 T T 串的第 i i 个位置,匹配了 S S 串的前 j j 个位置,现在要判断的是 T [ i ] T[i] S [ j + 1 ] S[j+1] 是否相等.

  • 如果相等,则直接 j j 指针加一即可,否则我们可以令 j = n e x t [ j ] j=next[j] ,这里就完美体现了KMP的巧妙之处.

  • 即不浪费每一次的比较.

next[1] = 0;
for (int i = 2, j = 0; i <= n; i ++) {
	while (j > 0 && a[i] != a[j + 1]) j = next[j];
	if (a[i] == a[j + 1]) j ++;
	next[i] = j;
}

for (int i = 1, j = 0; i <= m; i ++) {
	while (j > 0 && (j == n || b[i] != a[j + 1])) j = next[j];
	//这里需要注意,我们要找出所有S在T中出现位置,如果已经j==n了,则必须把j==next[j]
	if (b[i] == a[j + 1]) j ++;
	if (j == n)
		S在T中出现了一次.
}

Hash

  • Hash在NOIP范围内运用的很多,如【NOIP2014day2】解方程一题,运用普通Hash可以拿到很不错的分数.

  • 虽然正解更加巧妙,但同样是运用了Hash的思想

  • 一般来说,Hash可以看做是一个表,每个数 x x H ( x ) = ( x   m o d   P ) + 1 H(x)=(x\ mod\ P)+1 形式得到的值存进去,其中 P P 一般是一个很大的质数,因为不能保证没有冲突,所以一般Hash都会与链表同在,以保证时间复杂度.

  • 但是在许多Hash种,如字符串Hash,我们一般是拿Hash值来进行比较,而并非存到Hash表中.

  • 所以很常见的一种方法是按进制处理,然后自然溢出(用unsigned long long类型储存即可),把字符串看成 P P 进制,其中 P P 131 13331 131 或 13331 的时候效果很好.

  • 来看一道例题:jzoj5462

  • 我们直接运用上述的方法,代码如下:

#include <cstdio>

#define F(i, a, b) for (int i = a; i <= b; i ++)

const int N = 2e5 + 10,
	beed1 = 13331, beed2 = 131, k1 = 1231231, k2 = 1123513,
	m1 = 123456, m2 = 654321;

int n, m, Ans; bool bz1[m1 + 10], bz2[m2 + 10];
unsigned long long F1[N], F2[N], B1, B2, x, y;
char ch[N];

int main() {
	freopen("article.in", "r", stdin);
	freopen("article.out", "w", stdout);
	scanf("%d %d %s", &n, &m, ch + 1);
	B1 = B2 = 1;
	F(i, 1, m)
		B1 *= beed1, B2 *= beed2;
	F(i, 1, n) {
		F1[i] = F1[i - 1] * beed1 + (ch[i] - 'a') + k1;
		F2[i] = F2[i - 1] * beed2 + (ch[i] - 'a') + k2;
	}
	F(i, m, n) {
		x = F1[i] - F1[i - m] * B1; x = x % m1;
		y = F2[i] - F2[i - m] * B2; y = y % m2;
		if (!bz1[x] && !bz2[y])
			Ans ++;
		bz1[x] = bz2[y] = 1;
	}
	printf("%d\n", Ans);
}
  • 运用了自然溢出取模,但实际得分令人震惊:在这里插入图片描述

  • 然后我尝试把模数开大一点,这时候分数依然令人震惊:

在这里插入图片描述

  • 然后我再尝试着开5个自然溢出的数组,这时候我才发现,原来代码的这一个地方打错了,&&应该改为||.
if (!bz1[x] && !bz2[y])
	Ans ++; //这是错误的
bz1[x] = bz2[y] = 1;

if (!bz1[x] || !bz2[y])
	Ans ++; //这才正确
bz1[x] = bz2[y] = 1;
  • 但我发现,即使改正过后,并且多开很多个数组进行判断,拿的分依旧不超过70分

  • 证明,自然取模虽然简单好用,但在特殊构造的数据下,极其容易出错.

  • 70分的代码如下:

#include <cstdio>

#define F(i, a, b) for (int i = a; i <= b; i ++)

const unsigned long long N = 2e5 + 10,
	beed1 = 13331, beed2 = 131, beed3 = 123123153, beed4 = 998244353,
	k1 = 6522331231, k2 = 5549260917, k3 = 23121451532, k4 = 915398244353,
	m1 = 5123456, m2 = 6543321, m3 = 1231231, m4 = 7812434;

int n, m, Ans; bool bz1[m1 + 10], bz2[m2 + 10], bz3[m3 + 10], bz4[m4 + 10];
unsigned long long F1[N], F2[N], F3[N], F4[N], B1, B2, B3, B4, x, y, X, Y;
char ch[N];

int main() {
	freopen("article.in", "r", stdin);
	freopen("article.out", "w", stdout);
	scanf("%d %d %s", &n, &m, ch + 1);
	B1 = B2 = B3 = B4 = 1;
	F(i, 1, m)
		B1 *= beed1, B2 *= beed2, B3 *= beed3, B4 *= beed4;
	F(i, 1, n) {
		F1[i] = F1[i - 1] * beed1 + (ch[i] - 'a') + k1;
		F2[i] = F2[i - 1] * beed2 + (ch[i] - 'a') + k2;
		F3[i] = F3[i - 1] * beed3 + (ch[i] - 'a') + k3;
		F4[i] = F4[i - 1] * beed4 + (ch[i] - 'a') + k4;
	}
	F(i, m, n) {
		x = F1[i] - F1[i - m] * B1; x = x % m1;
		y = F2[i] - F2[i - m] * B2; y = y % m2;
		X = F3[i] - F3[i - m] * B3; X = X % m3;
		Y = F4[i] - F4[i - m] * B4; Y = Y % m4;
		if (!bz1[x] || !bz2[y] || !bz3[X] || !bz4[Y])
			Ans ++;
		bz1[x] = bz2[y] = bz3[X] = bz4[Y] = 1;
	}
	printf("%d\n", Ans);
}
  • 其中后三个数据,与答案的差距非常大,观察数据后发现,其只由两个字母构成,且有循环节,并且针对了自然溢出出数据.

  • 我们尝试着用普通的取模进行操作,然后随便取几个模数,就可以过了:

#include <cstdio>

#define F(i, a, b) for (int i = a; i <= b; i ++)

const long long
	N = 2e5 + 10,
	beed1 = 13331, beed2 = 131, beed3 = 123123153, beed4 = 998244353, beed5 = 123456789,
	k1 = 6522331231, k2 = 5549260917, k3 = 23121451532, k4 = 915398244353, k5 = 13515315347,
	m1 = 9123456, m2 = 8543321, m3 = 8231231, m4 = 7812434, m5 = 9154782;

int n, m, Ans; bool bz1[m1 + 10], bz2[m2 + 10], bz3[m3 + 10], bz4[m4 + 10], bz5[m5 + 10], bz;
long long F1[N], F2[N], F3[N], F4[N], F5[N], B1, B2, B3, B4, B5, x, y, X, Y, A;
char ch[N], S[N];

int main() {
	freopen("article.in", "r", stdin);
	freopen("article.out", "w", stdout);

	scanf("%d %d %s", &n, &m, ch + 1);
	scanf("%s", S + 1);

	B1 = B2 = B3 = B4 = B5 = 1;
	F(i, 1, m)
		B1 = (B1 * beed1) % m1,
		B2 = (B2 * beed2) % m2,
		B3 = (B3 * beed3) % m3,
		B4 = (B4 * beed4) % m4,
		B5 = (B5 * beed5) % m5;

	F(i, 1, n) {
		F1[i] = (F1[i - 1] * beed1 + (ch[i] - 'a') * k1) % m1;
		F2[i] = (F2[i - 1] * beed2 + (ch[i] - 'a') * k2) % m2;
		F3[i] = (F3[i - 1] * beed3 + (ch[i] - 'a') * (ch[i] - 'a') * k3) % m3;
		F4[i] = (F4[i - 1] * beed4 + (ch[i] - 'a') * (ch[i] - 'a') * (ch[i] - 'a') * k4) % m4;
		F5[i] = (F5[i - 1] * beed5 + (ch[i] - 'a') * k5) % m5;
	}
	F(i, m, n) {
		x = ((F1[i] - F1[i - m] * B1) % m1 + m1) % m1;
		y = ((F2[i] - F2[i - m] * B2) % m2 + m2) % m2;
		X = ((F3[i] - F3[i - m] * B3) % m3 + m3) % m3;
		Y = ((F4[i] - F4[i - m] * B4) % m4 + m4) % m4;
		A = ((F5[i] - F5[i - m] * B5) % m5 + m5) % m5;
		if (!bz1[x] || !bz2[y] || !bz3[X] || !bz4[Y] || !bz5[A])
			Ans ++;
		bz1[x] = bz2[y] = bz3[X] = bz4[Y] = bz5[A] = 1;
	}

	printf("%d\n", Ans);
}

强联通分量

  • 我对这个知识点的定义是一个比较简单,但却很容易打错的算法.

  • 这个算法没有必要赘述(主要是赘述不清

  • 求有向图当中强联通分里个数代码:

#include <cstdio>

#define F(i, a, b) for (int i = a; i <= b; i ++)
#define mn(a, b) ((a) = (a) < (b) ? (a) : (b))

const int N = 2e5 + 10;

using namespace std;

int n, x, y, cnt, num, top; bool ins[N];
int dfn[N], low[N], tov[N], nex[N], las[N], stack[N], tot;

void link(int x, int y) { tov[++ tot] = y, nex[tot] = las[x], las[x] = tot; }

void Tarjan(int k) {
	dfn[k] = low[k] = ++ num;
	stack[++ top] = k, ins[k] = 1; //注意这个地方与接下来的点双边双有区别,要保证一个点在栈里面才进行下面的mn(low[k], dfn[tov[x]])操作.
	for (int x = las[k] ; x ; x = nex[x])
		if (!dfn[tov[x]]) {
			Tarjan(tov[x]);
			mn(low[k], low[tov[x]]);
		}
		else if (ins[tov[x]]) mn(low[k], dfn[tov[x]]);
	if (dfn[k] == low[k]) {
		++ cnt;
		while (k ^ (y = stack[top --]))
			ins[y] = 0;
	}
}

int main() {
	scanf("%d", &n);
	F(i, 1, n)
		scanf("%d", &x), link(i, x);
	F(i, 1, n)
		if (!dfn[i]) Tarjan(i);
	printf("%d\n", cnt);
}

猜你喜欢

转载自blog.csdn.net/Algor_pro_king_John/article/details/83861926