Codeforces 833B 题解

博客介绍了如何解决Codeforces 833B问题,通过维护区间不同数种类数的技巧求解,利用线段树优化状态转移,达到O(nk)的时间复杂度,最大化序列划分的得分。文章强调了这个区间维护方法的重要性,并给出了思路和解释。
摘要由CSDN通过智能技术生成

题意简述

给定一个序列 a a a,长度 < = 35000 <=35000 <=35000。定义一个区间 [ l , r ] [l,r] [l,r]的得分为这段区间内不同的数的个数。请你将这个序列划分成 k ( < = 50 ) k(<=50) k(<=50)段,使得每段的分数加起来最大。

思路

d p [ j ] [ i ] dp[j][i] dp[j][i]表示前 i i i个分成了 j j j段的最大得分和, d ( l , r ) d(l,r) d(l,r)表示 [ l , r ] [l,r] [l,r]内不同的数个数,那么
(这里不知为啥公式爆了,看图凑合一下),其中 p p p枚举前面的一个点(即 1 < = p < i 1<=p<i 1<=p<i)。
珂是这个转移大概是 O ( n 2 k ) O(n^2k) O(n2k)的,完全不能承受。

观察数据范围,发现时间复杂度大概是 O ( n k ) O(nk) O(nk),或者带个 l o g log log,根号之类的。

不能用状态数优化这个 D P DP DP,那就考虑用数据结构优化。发现 d p [ j ] [ i ] dp[j][i] dp[j][i]只和 d p [ j − 1 ] [ ∗ ] dp[j-1][*] dp[j1][]有关(这就是为什么我把 j j j放到了第一维而不是第二维)。

那么 d p [ i ] dp[i] dp[i]依赖的状态就只有一个序列了。然后我们就是要求这个序列的整体最大值,其中第 p p p个点的权值是 d p [ j − 1 ] [ p ] + d ( p + 1 , j ) dp[j-1][p]+d(p+1,j) dp[j1][p]+d(p+1,j)

我们珂以把这个东西想象成这样:初始每个点都是 d p [ j − 1 ] [ p ] dp[j-1][p] dp[j1][p],然后通过一些修改操作(区间加)变成 d p [ j − 1 ] [ p ] + d ( p + 1 , j ) dp[j-1][p]+d(p+1,j) dp[j1][p]+d(p+1,j)

这样考虑,设 p r e [ i ] pre[i] pre[i]表示: a [ i ] a[i] a[i]上一次出现的位置 + 1 +1 +1。(如果没有上一次,那么 p r e [ i ] = 1 pre[i]=1 pre[i]=1)那么,对于一个种类 a [ i ] a[i] a[i],它的贡献就是把 [ p r e [ i ] , i ] [pre[i],i] [pre[i],i]区间 + 1 +1 +1。然后点 d ( p + 1 , j ) d(p+1,j) d(p+1,j)的值就是 [ 1 , p ] [1,p] [1,p]之间加上值的最大值了。

这个维护区间种类数的方式十分常见!!!请各位记好这个trick!!!

(然后解释一下为什么是这样的,如果你能想明白,跳过)
假设 a a a序列是这样的:

1 5 2 1 4 5

易得 p r e pre pre数组是这样的:

1 1 1 2 1 3

然后我们考虑第一个位置。区间加 [ p r e [ 1 ] , 1 ] [pre[1],1] [pre[1],1],也就是 [ 1 , 1 ] [1,1] [1,1]
然后就是区间加
[ 1 , 2 ] [1,2] [1,2]
[ 1 , 3 ] [1,3] [1,3]
[ 2 , 4 ] [2,4] [2,4]
[ 1 , 5 ] [1,5] [1,5]
[ 3 , 6 ] [3,6] [3,6]
加完之后我们发现区间变成了这样:

5 4 3 2 1 1

其中这个序列的第 i i i个位置表示 [ i , e n d ] [i,end] [i,end] e n d end end是最后一个位置)中有多少不同的种类。
区间加 [ p r e [ 1 ] , 1 ] [pre[1],1] [pre[1],1]的意义是:在 [ p r e [ 1 ] , 1 ] [pre[1],1] [pre[1],1]区间内,每个位置到末尾都能包含 a [ 1 ] a[1] a[1]
区间加 [ p r e [ 2 ] , 2 ] [pre[2],2] [pre[2],2]的意义是:在 [ p r e [ 2 ] , 2 ] [pre[2],2] [pre[2],2]区间内,每个位置到末尾都能包含 a [ 2 ] a[2] a[2]

区间加 [ p r e [ 6 ] , 6 ] [pre[6],6] [pre[6],6]的意义是:在 [ p r e [ 6 ] , 6 ] [pre[6],6] [pre[6],6]区间内,每个位置到末尾都能包含 a [ 6 ] a[6] a[6]
也许你会说:那第一个位置到最后一个位置不是也能包含 a [ 6 ] a[6] a[6]么,但是这一段在算上一个和 a [ 6 ] a[6] a[6]相同的数(即 a [ 2 ] a[2] a[2])的时候,已经被加过了。如果再加一次,就重复了。
(也就是说,只加 p r e [ i ] pre[i] pre[i] i i i这一段保证了不会重复,而且也不可能会有遗漏)
(顺便说一句,举一个实例解释问题是很有效的)

然后我们发现,我们珂以拿线段树维护这个东西。对于一个 d p [ j ] [ i ] dp[j][i] dp[j][i],初始值是 d p [ j − 1 ] [ 1   i ] dp[j-1][1~i] dp[j1][1 i],然后区间加即可,最后 d p [ j ] [ i ] dp[j][i] dp[j][i]的值就是查询区间 [ 1 , j ] [1,j] [1,j]的最大值。

代码:

#include<bits/stdc++.h>
using namespace std;
namespace Flandre_Scarlet
{
	#define N 45000
	
	#define MEM(x,a) memset(x,a,sizeof(x))
    #define FK(x) MEM(x,0)
    #define F(i,l,r) for(int i=l;i<=r;++i)
    #define D(i,r,l) for(int i=r;i>=l;--i)
	int n,k,a[N];
	void R1(int &x)
    {
        x=0;char c=getchar();int f=1;
        while(c<'0' or c>'9') f=(c=='-')?-1:1,c=getchar();
        while(c>='0' and c<='9') x=(x<<1)+(x<<3)+(c^48),c=getchar();
        x=(f==1)?x:-x;
    }
	void Input()
	{
		R1(n),R1(k);
		F(i,1,n) R1(a[i]);
	}
	
	int dp[51][N];
	class SegmentTree
	{
		public:
			struct node
			{
				int l,r;
				int s,a;
			}tree[N<<2];
			#define ls (index<<1)
			#define rs (index<<1|1)
			
			#define L tree[index].l
			#define R tree[index].r
			#define S tree[index].s
			#define A tree[index].a
			
			#define lL tree[ls].l
			#define lR tree[ls].r
			#define lS tree[ls].s
			#define lA tree[ls].a
			
			#define rL tree[rs].l
			#define rR tree[rs].r
			#define rS tree[rs].s
			#define rA tree[rs].a
			
			void Update(int index)
			{
				S=max(lS,rS);
			}
			void Build(int pos,int l,int r,int index)
			{
				L=l,R=r,S=0,A=0;
				if (l==r) 
				{
					S=dp[pos][l-1];
					return;
				}
				int mid=(l+r)>>1;
				Build(pos,l,mid,ls);
				Build(pos,mid+1,r,rs);
				Update(index);
			}
			void AddOne(int x,int index)
			{
				S+=x;A+=x;
			}
			void PushDown(int index)
			{
				if (A)
				{
					AddOne(A,ls);
					AddOne(A,rs);
					A=0;
				}
			}
			void Add(int l,int r,int x,int index)
			{
				if (l>R or L>r) return;
				if (l<=L and R<=r) return AddOne(x,index);
				PushDown(index);
				Add(l,r,x,ls);
				Add(l,r,x,rs);
				Update(index);
			}
			int Query(int l,int r,int index)
			{
				if (l>R or L>r) return 0;
				if (l<=L and R<=r) return S;
				PushDown(index);
				return max(Query(l,r,ls),Query(l,r,rs));
			}
	}T;
	int pos[N];
	int pre[N];
	void Soviet()
	{
		F(i,1,n)
		{
			pre[i]=pos[a[i]]+1;
			pos[a[i]]=i;
		}
		F(i,1,k)
		{
			T.Build(i-1,1,n,1);
			F(j,1,n)
			{
				T.Add(pre[j],j,1,1);
				dp[i][j]=T.Query(1,j,1);
			}
		}
		printf("%d\n",dp[k][n]);
	}
	void IsMyWife()
	{
		Input();
		Soviet();
	}
}
int main()
{
	Flandre_Scarlet::IsMyWife();
	return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值