树链剖分
-
这是一个很简单的知识点.
-
主要思想是把树按照重链进行剖分.
-
重链指的是一条由顶点与重儿子相连,再由重儿子与重儿子的重儿子相连……组成的链。
-
而重儿子则指的是一个点所有儿子中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树
-
给出一颗包含 个节点的** 叉树**,其中第 个叶子节点带权 ,要求最小化 ,其中 表示第 个叶子结点到根节点的距离。该问题的解被称为 叉
Huffman
树 -
其实很早以前就已经接触过这个了,当时做过很经典的一道题:JZOJ4210
Problem
-
给定一个非严格递减序列 , .
-
现在有一个 网络,左下角 ,右上角 ,要求从 .
-
走格方式: , ,其中后者要支付 代价.
-
求最小代价。
Solution
-
可以感受到这道题的 很类似二叉树上的某种操作,我们尝试着进行类比.
-
考虑一个非严格递减的序列 ,它的
Huffman
树应该如何构造。 -
我们令
f[i][j]
表示当前Huffman
树构造到序列的第 个位置,即序列的前 个位置已经放好,现在准备放 到Huffman
树上,有 个叶子节点可以供放 这个数. -
注意前提 是降序的, 所以不难发现,每个点对应在
Huffman
树上的深度必定是升序的. -
所以不难写出转移:
-
含义分别是,把当前
Huffman
树上所有叶节点都扩展两个新叶节点,拿来供给以后 的节点放,但因为深度多了 ,所以不管怎样,以后的节点都至少要多一个 贡献,所以相当于加上一个 ,另外一个转移则是直接把当前 号节点放在 个叶节点的某一节点中. -
整个流程如下:
#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
树解的询问,这个可以在 时间内解决. -
代码如下:
#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
树. -
如果讨论 叉树,方法也是类似的,唯一需要注意的是必须通过在序列里补 的方式把 变为 的倍数.
-
这是由于,我们必须让每次在
set
里选 个时,不能选满的一次,必须发生在Huffman
树的最底层. -
至此,有关哈夫曼树的问题暂且告一段落.
KMP
-
即模式匹配,能够在线性时间内求出字符串 在字符串 中所有出现过的位置.
-
其核心是 数组
-
表示 .
-
考虑普通的匹配,我们总是拿 串当中的某一位置与 串某一位置进行比较,不妨设当前枚举到 串的第 个位置,匹配了 串的前 个位置,现在要判断的是 与 是否相等.
-
如果相等,则直接 指针加一即可,否则我们可以令 ,这里就完美体现了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
可以看做是一个表,每个数 以 形式得到的值存进去,其中 一般是一个很大的质数,因为不能保证没有冲突,所以一般Hash
都会与链表同在,以保证时间复杂度. -
但是在许多
Hash
种,如字符串Hash
,我们一般是拿Hash
值来进行比较,而并非存到Hash
表中. -
所以很常见的一种方法是按进制处理,然后自然溢出(用
unsigned long long
类型储存即可),把字符串看成 进制,其中 取 的时候效果很好. -
来看一道例题: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);
}
-
综上所述,带取模的hash是很难被卡的,然而直接自然溢出是极容易被卡的.
-
其中,对于如何卡自然溢出,以及不用双hash或者多hash的卡法,在这片题解中写的很详尽
-
https://jzoj.net/senior/index.php/main/download/5462/article.pdf/0/solution_path
强联通分量
-
我对这个知识点的定义是一个比较简单,但却很容易打错的算法.
-
这个算法没有必要赘述(
主要是赘述不清) -
求有向图当中强联通分里个数代码:
#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);
}