题解: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;
}

浙公网安备 33010602011771号