题解:P3201 [HNOI2009] 梦幻布丁

题目链接

https://www.luogu.com.cn/problem/P3201

题目简介

P3201 [HNOI2009] 梦幻布丁

题目描述

\(n\) 个布丁摆成一行,进行 \(m\) 次操作。每次将某个颜色的布丁全部变成另一种颜色的,然后再询问当前一共有多少段颜色。

\(1 \leq n, m \leq 10^5\)\(1 \leq a_i ,x, y \leq 10^6\)

解题思路

这道题非常有意思,卡了我两个小时。

总体来说这道题非常适合萌新去练习启发式合并。

我们把每种颜色的布丁放入一个集合,我们每次操作 \(1\) 就需要把这两个集合合并,并且计算这次合并对于答案的贡献。正常情况下我们每次合并集合 \(x\)\(y\) 需要 \(O(|x|)\) 的时间复杂度,当集合 \(x\) 的大小过大时时间复杂度也会上升,我们考虑启发式合并的思想,每次把小的集合并到大的集合上面,可以有效降低复杂度。

我们分析启发式合并给我们带来的优化。

考虑用贡献法来分析。我们令两个集合的分别为 \(A\)\(B\),且 \(∣A∣<∣B∣\),那么我们把 \(A\) 暴力加入到 \(B\) 中。那么 \(A\) 中的元素所在的集合大小变成 \(∣A∣+∣B∣\),也就是说至少变成了原来的两倍。所以每个元素至多被加入 \(logn\) 次,总的复杂度为 \(O(nlogn)\)

对于这道题,我们求出原序列的答案后将每一种颜色都用链表串起来,每次修改都根据启发式合并的方法来暴力合并,然后处理此次合并的贡献即可。

AC代码

#include<bits/stdc++.h>
#define debug(a) cout<<#a<<"="<<a<<'\n';
#define il inline
// #define int long long

using namespace std;
using ll = long long;
using ull = unsigned long long;

const int maxn = 1e5+10;
const int maxm = 1e6+10;

int x,y,opt,n,m,c[maxn],sz[maxm],st[maxm],f[maxm],hd[maxm],nxt[maxn],ans;

void merge(int x,int y)
{
	for(int i = hd[x];i;i = nxt[i])
	ans -= (c[i-1] == y) + (c[i+1] == y);
	//如果合并这两个集,求对答案的贡献

	for(int i = hd[x];i;i = nxt[i])c[i] = y;//合并

	//将原集并到新集合上
	nxt[st[y]] = hd[x];
	st[y] = st[x];
	sz[y] += sz[x];

	//一定要初始化!!!
	sz[x] = 0;
	hd[x] = 0;
	st[x] = 0;
}

int main(){
	// freopen("test.in","r",stdin);
	// freopen("test.out","w",stdout);
	ios::sync_with_stdio(0),cout.tie(0),cin.tie(0);
	cin>>n>>m;

	for(int i = 1;i <= n;i++)
	{
		cin>>c[i];f[c[i]] = c[i];//处理原始集合
		ans += (c[i] != c[i-1]);//计算原序列的答案
		if(!hd[c[i]])st[c[i]] = i;//如果说遇到新颜色,就初始化一个新集合去记录这个颜色
		nxt[i] = hd[c[i]];//指向上一个元素
		hd[c[i]] = i;//记录当前元素的位置
		sz[c[i]]++;//当新元素加入,原集合大小增加
	}

	while(m--)
	{
		cin>>opt;
		if(opt == 1){
			cin>>x>>y;
			if(x == y)continue;
			if(sz[f[x]] > sz[f[y]]) swap(f[x],f[y]);//启发式合并
			if(!sz[f[x]])continue;
			merge(f[x],f[y]);
		}else{
			cout<<ans<<'\n';
		}
	}

	return 0;
}
posted @ 2025-03-10 21:27  Zheng_iii  阅读(38)  评论(0)    收藏  举报