这是一个很简单的知识点.
主要思想是把树按照重链进行剖分.
重链指的是一条由顶点与重儿子相连,再由重儿子与重儿子的重儿子相连……组成的链。
而重儿子则指的是一个点所有儿子中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];
给出一颗包含 n n n个节点的** k k k叉树**,其中第 i i i个叶子节点带权 w i w_i wi,要求最小化 ∑ w i ∗ l i \sum w_i*l_i ∑wi∗li,其中 l i l_i li表示第 i i i个叶子结点到根节点的距离。该问题的解被称为 k k k叉Huffman
树
其实很早以前就已经接触过这个了,当时做过很经典的一道题:JZOJ4210
给定一个非严格递减序列 A A A, B i = ∑ j = i n A j B_i=\sum_{j=i}^nA_j Bi=∑j=inAj.
现在有一个 n ∗ n n*n n∗n网络,左下角 ( 1 , 1 ) (1,1) (1,1),右上角 ( n , n ) (n,n) (n,n),要求从 ( n , 1 ) → ( 1 , 1 ) (n,1)\rightarrow(1,1) (n,1)→(1,1).
走格方式: ( x , y ) → ( x − 1 , y + 1 ) (x,y)\rightarrow(x-1,y+1) (x,y)→(x−1,y+1), ( x , ⌈ y + 1 2 ⌉ ) (x,\lceil \frac{y+1}{2} \rceil) (x,⌈2y+1⌉),其中后者要支付 B x B_x Bx代价.
求最小代价。
可以感受到这道题的 y + 1 2 \frac{y+1}{2} 2y+1很类似二叉树上的某种操作,我们尝试着进行类比.
考虑一个非严格递减的序列 A A A,它的Huffman
树应该如何构造。
我们令f[i][j]
表示当前Huffman
树构造到序列的第 i i i个位置,即序列的前 i − 1 i-1 i−1个位置已经放好,现在准备放 A i A_i Ai到Huffman
树上,有 j j j个叶子节点可以供放 A i A_i Ai这个数.
注意前提 A 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]+k=i∑nak→f[i][2∗j] f [ i ] [ j ] → f [ i + 1 ] [ j − 1 ] f[i][j] \rightarrow f[i +1][j - 1] f[i][j]→f[i+1][j−1]
含义分别是,把当前Huffman
树上所有叶节点都扩展两个新叶节点,拿来供给以后 i ∼ n i\sim n i∼n的节点放,但因为深度多了 1 1 1,所以不管怎样,以后的节点都至少要多一个 a k a_k ak贡献,所以相当于加上一个 ∑ k = i n a k \sum_{k=i}^n a_k ∑k=inak,另外一个转移则是直接把当前 i i i号节点放在 j j j个叶节点的某一节点中.
整个流程如下:
#include
#include
#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) O(nlogn)时间内解决.
代码如下:
#include
#include
#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 k叉树,方法也是类似的,唯一需要注意的是必须通过在序列里补 0 0 0的方式把 n − 1 n-1 n−1变为 k − 1 k-1 k−1的倍数.
这是由于,我们必须让每次在set
里选 k k k个时,不能选满的一次,必须发生在Huffman
树的最底层.
至此,有关哈夫曼树的问题暂且告一段落.
即模式匹配,能够在线性时间内求出字符串 S S S在字符串 T T T中所有出现过的位置.
其核心是 n e x t next next数组
n e x t [ i ] next[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] S[1∼next[i]]=S[n−next[i]+1∼n].
考虑普通的匹配,我们总是拿 T T T串当中的某一位置与 S S S串某一位置进行比较,不妨设当前枚举到 T T T串的第 i i i个位置,匹配了 S S S串的前 j j j个位置,现在要判断的是 T [ i ] T[i] T[i]与 S [ j + 1 ] S[j+1] S[j+1]是否相等.
如果相等,则直接 j j j指针加一即可,否则我们可以令 j = n e x t [ j ] j=next[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
在NOIP范围内运用的很多,如【NOIP2014day2】解方程一题,运用普通Hash
可以拿到很不错的分数.
虽然正解更加巧妙,但同样是运用了Hash
的思想。
一般来说,Hash
可以看做是一个表,每个数 x x x以 H ( x ) = ( x m o d P ) + 1 H(x)=(x\ mod\ P)+1 H(x)=(x mod P)+1形式得到的值存进去,其中 P P P一般是一个很大的质数,因为不能保证没有冲突,所以一般Hash
都会与链表同在,以保证时间复杂度.
但是在许多Hash
种,如字符串Hash
,我们一般是拿Hash
值来进行比较,而并非存到Hash
表中.
所以很常见的一种方法是按进制处理,然后自然溢出(用unsigned long long
类型储存即可),把字符串看成 P P P进制,其中 P P P取 131 或 13331 131 或 13331 131或13331的时候效果很好.
来看一道例题:jzoj5462
我们直接运用上述的方法,代码如下:
#include
#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);
}
运用了自然溢出取模,但实际得分令人震惊:
然后我尝试把模数开大一点,这时候分数依然令人震惊:
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
#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
#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
#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);
}