题目描述:
题目标签:构造树+LCA/并查集+启发式合并
题目大意:给出半径为1…n的n个盘子和m根塔,要求每根塔上盘子的半径始终从底向上递减,一次操作可以将一根塔上的任意个盘子移动到另一个塔的顶部,令某一情形下的复杂度为将所有盘子移动到同一根塔上所需要的最小操作数。题目给出m-1次询问,每次询问时输出当前情形的复杂度并合并两根塔上的盘子到同一根塔上。
首先计算复杂度,复杂度等于满足 t [ i ] ≠ t [ i + 1 ] , i ∈ [ 1 , n − 1 ] t[i]\not=t[i+1],i\in[1,n-1] t[i]=t[i+1],i∈[1,n−1]的i的个数。考虑从半径为1的盘子开始,若和半径为2的盘子在同一根塔上则不需要进行操作,若和半径为2的盘子不在同一根塔上,则需要进行一次操作,将1移动至2所在的塔。当半径为1…k在一根塔上时,若半径为k+1和k在一根塔上,则不需要操作,若不在一根塔,则需要操作一次,将1…k移动到k+1所在的塔。
于是问题的关键在于解决每次合并两根塔后,能使多少个数满足 t [ i ] = t [ i + 1 ] t[i]=t[i+1] t[i]=t[i+1]。
官方题解还给出了一个将m个盘子和m-1次询问建树后求LCA的做法,也挺巧妙的。
但是实际上并查集就可以解决这个问题。关键在于每次合并时,比较两根塔上盘子的个数,选择塔上盘子较少的那个,对每个盘子的相邻盘子检查是否属于另一根塔,最后将这些盘子也并入另一根塔。这样即使考虑到最坏的情况(合并情况类似于一颗满二叉树),时间复杂度也是O(nlogm)的。
所以简而言之就是题解里面说的"small-to-large merging",合并时将小的并到大里,算是比较常见的启发式合并吧。
#include<bits/stdc++.h>
#define debug(x) cerr<<#x<<":"<<x<<endl;
using namespace std;
typedef long long ll;
typedef pair<int,int> pii;
const int MAXN=2e5+5;
int T,n,m,x,y,k,b,u,v;
vector<int>G[MAXN];
int A[MAXN];
int FA[MAXN];
int fnd(int x)
{
if (FA[x]==x) return x;
else return FA[x]=fnd(FA[x]);
}
void Union(int x,int y)
{
for (auto i:G[y]) G[x].push_back(i);
vector<int>tmp;
G[y].swap(tmp);
FA[fnd(y)]=fnd(x);
}
int ans,now;
int main()
{
cin>>n>>m;
for (int i=1;i<=n;i++)
{
scanf("%d",&A[i]);
G[A[i]].push_back(i);
if (i>1 && A[i]!=A[i-1]) ans++;
}
for (int i=1;i<=m;i++)
FA[i]=i;
printf("%d\n",ans);
for (int i=1;i<=m-1;i++)
{
scanf("%d%d",&u,&v);
u=fnd(u),v=fnd(v);
vector<int>T;
if (G[u].size()<G[v].size())
swap(u,v);
T=G[v];
for (auto x:T)
{
now=x-1;
if (now>=1 && now<=n)
if (fnd(A[now])==u) ans--;
now=x+1;
if (now>=1 && now<=n)
if (fnd(A[now])==u) ans--;
}
Union(u,v);
printf("%d\n",ans);
}
return 0;
}