算法模板(1):基础算法(2)

基础算法

启发式合并

  • 给了n个集合,把他们合并。对于每个元素,计算合并后的贡献

2154. 梦幻布丁

  • 题意:他将布丁摆成一行,接着说:“我可以把某种颜色的布丁全部变成另一种颜色,我还会在某些时刻问你当前一共有多少段颜色。例如:颜色分别为 1,2,2,1 的四个布丁一共有 3 段颜色。”

  • 当把 x 变为 y 时,只有 x 和 y 相邻的地方,才会使段数减少1.

  • 每次合并集合的时候采用按秩合并的方式,把 size 小的集合合并到 size 大的集合中,如果 s z [ 1 ] > s z [ 2 ] sz[1] > sz[2] sz[1]>sz[2],就把对应的两种颜色的指针互换(比如之前 1 → 1 , 2 → 2 1\rightarrow1, 2\rightarrow2 11,22,现在 1 → 2 , 2 → 1 1\rightarrow2,2\rightarrow1 12,21).

  • 因为每次都是把小集合并入大集合,因此只需要遍历小的集合每个元素,看看对答案的影响。整个复杂度是 O ( n ∗ l o g n ) O(n*logn) O(nlogn)

#include
#include
#include
using namespace std;
const int maxn = 100010, maxm = 1000010;

int N, M;
//存储链表,即颜色编号连接的苹果(位置编号)
//每个颜色作为一个头结点,最多maxm,每个位置连一条边,最多maxn条。
int h[maxm], e[maxn], ne[maxn], idx;
//每个位置的颜色,每种颜色的集合大小,每种颜色映射的哪个颜色编号。
int color[maxn], sz[maxm], p[maxm];
int ans;   //现在有多少段颜色。

void add(int a, int b) {
	e[idx] = b, ne[idx] = h[a], h[a] = idx++;
	sz[a]++;
}

void merge(int& x, int& y) {
	if (x == y) return;
	if (sz[x] > sz[y]) swap(x, y);
	for (int i = h[x]; i != -1; i = ne[i]) {
		int v = e[i];
		ans -= (color[v - 1] == y) + (color[v + 1] == y);
	}
	for (int i = h[x]; i != -1; i = ne[i]) {
		int v = e[i];
		color[v] = y;
		if (ne[i] == -1) {
			ne[i] = h[y], h[y] = h[x];
			break;
		}
	}
	//把x清空
	h[x] = -1;
	sz[y] += sz[x], sz[x] = 0;
}

int main() {
	scanf("%d%d", &N, &M);
	memset(h, -1, sizeof h);
	for (int i = 1; i <= N; i++) {
		scanf("%d", &color[i]);
		if (color[i] != color[i - 1]) ans++;
		add(color[i], i);
	}
	//一开始每种颜色的映射都是自己
	for (int i = 0; i < maxm; i++) p[i] = i;
	while (M--) {
		int op;
		scanf("%d", &op);
		if (op == 2) printf("%d\n", ans);
		else {
			int x, y;
			scanf("%d%d", &x, &y);
			merge(p[x], p[y]);
		}
	}
	return 0;
}

Lomsat gelral

  • 树上并查集
  • 给定一棵由 n 个节点组成的树。树的节点编号为 1∼n,其中 1 号节点为树的根节点。每个节点上都标有某种颜色 c i c_i ci. 如果在以节点 v 为根节点的子树中,没有任何颜色的出现次数超过颜色 c 的出现次数,那么我们称颜色 c 为该子树的主要颜色。一些子树的主要颜色可能不止一种。对于每个节点 v ( 1 ≤ v ≤ n ) v(1≤v≤n) v(1vn),请你求出以该节点为根节点的子树的所有主要颜色之和。
  • 重儿子:当前节点的以每个儿子为根的子树,最大的子树对应的儿子叫重儿子。
  • 从某个点到根节点的路径中,最多有 log ⁡ n \log n logn 条轻边.
#include
using namespace std;
typedef long long ll;

const int N = 100010, M = 2 * N;
int h[N], e[M], ne[M], idx;
int color[N], cnt[N], son[N], sz[N];

ll ans[N], sum, mx;

void add(int a, int b)
{
    e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}

int dfs_son(int u, int fa)
{
    sz[u] = 1;

    for(int i = h[u]; i != -1; i = ne[i])
    {
        int v = e[i];
        if(v == fa) continue;

        sz[u] += dfs_son(v, u);

        if(!son[u] || sz[v] > sz[son[u]]) son[u] = v;
    }

    return sz[u];
}

void update(int u, int fa, int sign, int pson)
{
    int c = color[u];
    cnt[c] += sign;
    if(cnt[c] > mx) mx = cnt[c], sum = c;
    else if(cnt[c] == mx) sum += c;

    for(int i = h[u]; i != -1; i = ne[i])
    {
        int v = e[i];
        if(v == fa || v == pson) continue;
        
        //这里传的参数仍然是pson不是son[v]
        //因为你要修改的是除了一个重儿子之外的所有结点,而不是所有重儿子都不修改
        update(v, u, sign, pson);
    }
}

void dfs(int u, int fa, int op)
{
    for(int i = h[u]; i != -1; i = ne[i])
    {
        int v = e[i];
        if(v == fa || son[u] == v) continue;
        dfs(v, u, 0);
    }
    if(son[u]) dfs(son[u], u, 1);
    update(u, fa, 1, son[u]);

    ans[u] = sum;

    if(!op) update(u, fa, -1, 0), sum = 0, mx = 0;
}

int main()
{
    memset(h, -1, sizeof h);
    int n;
    scanf("%d", &n);
    for(int i = 1; i <= n; i++)
    {
        scanf("%d", &color[i]);
    }
    for(int i = 1; i < n; i++)
    {
        int x, y;
        scanf("%d%d", &x, &y);
        add(x, y), add(y, x);
    }
    dfs_son(1, -1);
    dfs(1, -1, 1);

    for(int i = 1; i <= n; i++)
    {
        printf("%lld ", ans[i]);
    }
    return 0;
}

manacher算法

3188. manacher算法

  • 给定一个长度为 n n n 的由小写字母构成的字符串,求它的最长回文子串的长度是多少。
  • 第1步,先转化,把原字符串化成 $#a#b#c#^ 这个形式,寻找所有奇数长度子回文串,最后答案就是得到的最长的回文串的半径减1.
  • p i p_i pi 就是以 S i S_i Si 为中心的最大回文串的半径。
  • 马拉车算法有一个这样的性质,如果不进行扩展的话(即往中间加了很多#),只能找到所有长度为奇数的回文子串(因此,为了找到偶数回文子串,需要将字符串扩展)。但是需要注意的是,马拉车算法找到的回文子串不能保证子串的本质不同。如果要找到所有本质不同的字符串的话,需要借助字符串哈希。
#include
#include
#include
using namespace std;
const int maxn = 20000010;
char a[maxn], b[maxn];
int p[maxn], N;
void init() {
	int k = 0;
	b[k++] = '$', b[k++] = '#';
	for (int i = 0; i < N; i++) b[k++] = a[i], b[k++] = '#';
	b[k++] = '^';
	N = k;
}
void manacher() {
	int mr = 0, mid;
	for (int i = 1; i < N; i++) {
		if (i < mr) p[i] = min(p[2 * mid - i], mr - i);
		else p[i] = 1;
		while (b[i - p[i]] == b[i + p[i]]) p[i]++;
		if (i + p[i] > mr) {
			mr = i + p[i];
			mid = i;
		}
	}
}
int main() {
	scanf("%s", a);
	N = strlen(a);
	init();
	manacher();
	int res = 0;
	for (int i = 0; i < N; i++) res = max(res, p[i]);
	printf("%lld\n", res - 1);
	return 0;
}

最小表示法

158. 项链

  • 题意:输入文件只有两行,每行一个由字符0至9构成的字符串,描述一个项链的表示(保证项链的长度是相等的)。判断是否可能是一条项链。

  • 循环同构

    当字符串 S S S 中可以选定一个位置 i i i 满足

    S [ i ⋯ n ] + S [ 1 ⋯ i − 1 ] = T S[i\cdots n]+S[1\cdots i-1]=T S[in]+S[1i1]=T

    则称 S S S T T T 循环同构

  • 字符串 S S S 的最小表示为与 S S S 循环同构的所有字符串中字典序最小的字符串

  • 假如 s [ i : i + k ] = = s [ j : j + k ] s[i:i+k] == s[j:j+k] s[i:i+k]==s[j:j+k],但是 s [ i + k ] > s [ j + k ] s[i+k]>s[j+k] s[i+k]>s[j+k],那么从 i i i ~ i + k i+k i+k 开头的字符串,字典序一定比对应于从 j j j ~ j + k j + k j+k 开头的字符串要小,因此从 i i i i + k i + k i+k 开头的字符串都不是最小的,因此可以把 i i i 直接加上 k + 1 k+1 k+1 然后继续搜索就行.

#include
#include
#include
using namespace std;

const int maxn = 2000010;
char a[maxn], b[maxn];
int n;

int get_min(char s[]) {
	int i = 0, j = 1;
	while (i < n && j < n) {
		int k = 0;
		while (k < n && s[i + k] == s[j + k]) k++;
		if (k == n) break;
		if (s[i + k] > s[k + j]) i += k + 1;
		else j += k + 1;
		if (i == j) j++;
	}
	//最小表示开头的那个字母应该是一直不被更新。
	int k = min(i, j);
	s[k + n] = 0;
	return k;
}

int main() {
	scanf("%s%s", a, b);
	n = strlen(a);
	//把原串复制为之前的两倍。
	memcpy(a + n, a, n);
	memcpy(b + n, b, n);
	int x = get_min(a), y = get_min(b);
	if (strcmp(a + x, b + y)) printf("No\n");
	else{
		printf("Yes\n");
		printf("%s\n", a + x);
	}
	return 0;
}

你可能感兴趣的:(算法模板,算法,图论,数据结构,c++)