把博客园图标替换成自己的图标
把博客园图标替换成自己的图标end

P4065 [JXOI2017] 颜色 分析

题目概述

可怜有一个长度为 \(n\) 的正整数序列 \(A_i\),其中相同的正整数代表着相同的颜色。

现在可怜觉得这个序列太长了,于是她决定选择一些颜色把这些颜色的所有位置都删去。

删除颜色 \(i\) 可以定义为把所有满足 \(A_j = i\) 的位置 \(j\) 都从序列中删去。

然而有些时候删去之后,整个序列变成了好几段,可怜不喜欢这样,于是她想要知道有多少种删去颜色的方案使得最后剩下来的序列非空且连续。

例如颜色序列 \(\{1, 2, 3, 4, 5\}\),删除颜色 \(3\) 后序列变成了 \(\{1, 2\}\)\(\{4, 5\}\) 两段,不满足条件。而删除颜色 \(1\) 后序列变成了 \(\{2, 3, 4, 5\}\),满足条件。

两个方案不同当且仅当至少存在一个颜色 \(i\) 只在其中一个方案中被删去。

对于 \(100\%\) 的数据,保证 \(1 \le T,\sum n \le 3 \times 10^5, 1 \le A_i \le n\)

题意转化

求有多少个子区间满足里面出现过的数不能在外面出现。

分析

将每个位置搞随机值,但是要让同一种颜色的随机值和为 \(0\)

然后就做完了,\(sum_r-sum_{l-1}=0\Rightarrow sum_r=sum_{l-1}\),用一个桶就可以了。

代码

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <stdlib.h>
#include <cstring>
#include <vector>
#include <map>
#include <random>
#include <time.h>
#define int long long
#define N 300005
#define random(l,r) (rd() % (r - (l) + 1) + l)
using namespace std;
int n,a[N],pre[N],sum[N],b[N];
map<int,int> mp;
mt19937_64 rd(time(0));
void solve(){
	mp.clear();
	scanf("%lld",&n);
	for (int i = 1;i <= n;i ++) scanf("%lld",&a[i]),pre[i] = sum[i] = 0;
	for (int i = 1;i <= n;i ++)	{
		if (pre[a[i]]) {
			b[pre[a[i]]] = random(1,10000000000000);
			sum[a[i]] += b[pre[a[i]]];
		}
		pre[a[i]] = i;
	}
	for (int i = 1;i <= n;i ++)
		if (pre[i]) b[pre[i]] = -sum[i];
	int ans = 0;
	mp[0] ++;
	for (int i = 1,x = 0;i <= n;i ++) {
		x = (b[i] += b[i - 1]);
		ans += mp[x];
		mp[x] ++;
	}
	printf("%lld\n",ans);
}
signed main(){
	int T;
	cin >> T;
	for (;T--;) solve();
	return 0;
}

分析2

显然,这道题目的魅力不止在随机化上面,还在于线段树上的经典应用——求点对贡献。

把区间看作点对,那么我们就算是处理点对的贡献。

对于这种题目都是枚举一个点,然后用线段树处理另外一个点。

在这道题目当中我们枚举 \(r\),现在只需要考虑对于每一个 \(l\),我们如何确定它的合法区间,然后进一步统计即可。

显然先考虑右边确定已经要删除的点,然后找到这些删除颜色中 \(<r\) 中的最大的。

具体的,设 \(max_i\) 表示颜色 \(i\) 最右边出现,\(min_i\) 表示颜色 \(i\) 最左边出现。那么我们需要找到最大的 \(j\) 满足 \(j<i,max_{a_j}>i\)

然后我们观察到对于左边这些东西,任意一个在这个区间的颜色 \(k\),我们选择左端点的时候都必须满足不选择 \((min_k,max_k]\) 这个区间,因为会把它们分开。

那我们从小到大枚举右端点 \(r\),然后对于 \(r=max_{a_r}\),则线段树上区间 \((min_k,max_k]\) 修改为 \(1\)(最后减去即可,还不用管重叠部分)。

现在我们需要考虑怎么求出最大的 \(j\),显然我们只需要维护一个下标单调递增且 \(max>i\) 即可,一旦过时直接删除即可,显然用栈即可。

得出 \(j\) 之后答案就是 \(r-j-t\)\(t\) 为区间 \([j+1,r]\) 的个数。

代码2

//M_sea 评价:左开右闭线段树
#include<cstdio>
#include<algorithm>
#include<stack>
#include<vector>
using namespace std;const int N=3*1e5+10;typedef long long ll;
int n;int mi[N];int ma[N];int cnt;int col[N];ll res;int T;
struct linetree//资瓷区间求和,区间赋值 
{
	int val[4*N];int sev[4*N];
	inline void pushdown(int p,int len)
	{if(sev[p]){val[p<<1]=len/2;val[p<<1|1]=len-len/2;sev[p<<1]=1;sev[p<<1|1]=1;}}
	void setval(int p,int l,int r,int dl,int dr)
	{
		if(dl==l&&dr==r){val[p]=r-l;sev[p]=1;return;}
		int mid=(l+r)/2;pushdown(p,r-l);
		if(dl<mid){setval(p<<1,l,mid,dl,min(mid,dr));}
		if(mid<dr){setval(p<<1|1,mid,r,max(dl,mid),dr);}
		val[p]=val[p<<1]+val[p<<1|1];
	}
	int sum(int p,int l,int r,int dl,int dr)
	{
		if(dl==l&&dr==r){return val[p];}
		int mid=(l+r)/2;int res=0;pushdown(p,r-l);
		if(dl<mid){res+=sum(p<<1,l,mid,dl,min(dr,mid));}
		if(mid<dr){res+=sum(p<<1|1,mid,r,max(dl,mid),dr);}
		return res;
	}
}lt;
struct data{int col;int pos;};stack <data> s;//开了一个栈 
inline void clear(stack <data>& st){stack <data> emp;swap(emp,st);}
inline void solve()
{
	scanf("%d",&n);
	for(int i=1;i<=n;i++){scanf("%d",&col[i]);}
	for(int i=1;i<=n;i++){mi[i]=0x3f3f3f3f;ma[i]=0;}
	for(int i=1;i<=4*n;i++){lt.val[i]=0;lt.sev[i]=0;}
	for(int i=1;i<=n;i++){mi[col[i]]=min(mi[col[i]],i);}//处理min 
	for(int i=1;i<=n;i++){ma[col[i]]=max(ma[col[i]],i);}//处理max 
	for(int i=1;i<=n;i++)//开始枚举右端点 
	{
		if(i==ma[col[i]]&&ma[col[i]]!=mi[col[i]])//如果是右端点的话就区间赋值表示禁用 
		{lt.setval(1,0,n,mi[col[i]],ma[col[i]]);}
		else {s.push((data){col[i],i});}
		for(;!s.empty()&&ma[s.top().col]<=i;s.pop());//找到左端点下限l 
		int l=(s.empty())?0:s.top().pos;//如果栈是空的话意味着所有左端点都可能合法 
		if(i!=l){res+=i-l-lt.sum(1,0,n,l,i);}//然后减去禁用的点数就好了 
	}printf("%lld\n",res);res=0;clear(s);//记得清空 
}
int main(){scanf("%d",&T);for(int z=1;z<=T;z++){solve();}return 0;}//拜拜程序~ 
posted @ 2026-07-23 21:37  high_skyy  阅读(2)  评论(0)    收藏  举报
动态线条
动态线条end
浏览器标题切换
浏览器标题切换end
💬 加载中……