P2418 yyy loves OI IV

题目传送门

推销一下博客

30pts 做法

首先想想 \(O(n^2)\) 做法。

我们设 \(dp_{i}\) 表示考虑了前 \(i\) 个人后,最少分多少个宿舍。

那为什么这样 dp 是正确的呢?

我们发现,如果前 \(i\) 个人的最少分宿舍方案有了,那么 \(i\) 以后的人不论怎么分都不会影响到 \(i\) 的分法。也就是满足无后效性。

并且,最小的 \(dp_i\) 肯定是由最小且符合条件的一个 \(dp_j\) 转移过来。也就是满足最优子结构。

这个暴力 dp 的式子很好写,我们枚举 \([1,i]\) 中的一个 \(j\),也就是去找最后一个宿舍 \([j,i]\) 的分发,转移方程式就是 \(dp_i = \min_{j=1}^{i}{dp_j}+1\),其中 \([j,i]\) 宿舍要满足条件。

至于如何去体现这个 \([j,i]\) 宿舍合法,我们记 \(sum_{1,i}\) 为前 \(i\) 个人里有多少膜拜 yyy 大神的,同理记 \(sum_{2,i}\) 为前 \(i\) 个人里有多少膜拜 c01 大神的。

那么 \([j,i]\) 这个区间里膜拜 yyy 大神的人数是 \(sum_{1,i}-sum_{1,j-1}\),膜拜 c01 大神的人数是 \(sum_{2,i}-sum_{2,j-1}\)

我们要满足的条件就是 \(-m \le (sum_{1,i}-sum_{1,j-1})-(sum_{2,i}-sum_{2,j-1}) \le m\) 或者 \(sum_{2,i}-sum_{2,j-1}=0\) 或者 \(sum_{1,i}-sum_{1,j-1}=0\)。(千万不要忘记同时膜拜一个大神的情况!!!)

这样就是我们的 30 分代码了,可以通过本题的弱化版

30分
#include<bits/stdc++.h>
#define int long long
using namespace std;

inline int read(){
	int x=0,f=1;char c=getchar();
	while(c<48){
		if(c=='-') f=-1;
		c=getchar();
	}
	while(c>47) x=(x<<1)+(x<<3)+(c^48),c=getchar();
	return x*f;
}

const int N=5e5+5;
const int inf=1e16;
int n,m,a[N],dp[N],sum[N];

signed main(){
	n=read(),m=read();
	for(int i=1;i<=n;i++){
		a[i]=read();
	}
	//和题解略有不同,sum[i]记1~i里2的个数 
	for(int i=1;i<=n;i++){
		sum[i]=sum[i-1]+a[i]-1;
		dp[i]=inf;
	}
	for(int i=1;i<=n;i++){
		for(int j=1;j<=i;j++){//[j,i]进一个宿舍 
			int cnt2=sum[i]-sum[j-1];//膜拜c01的人 
			int cnt1=i-j+1-cnt2;//膜拜yyy的人 
			if(abs(cnt1-cnt2)<=m||!cnt1||!cnt2){
				dp[i]=min(dp[i],dp[j-1]+1);
			}
		}
	}
	printf("%lld",dp[n]);
	return 0;
}

100pts 做法

考虑优化刚才的思路。我们发现转移式很简单,最麻烦的在于这个判定式。于是我们考虑化简判定式。

先看一下原本的式子:

\[-m \le (sum_{1,i}-sum_{1,j-1})-(sum_{2,i}-sum_{2,j-1}) \le m \]

我们考虑将 \(i\)\(j-1\) 分开:

\[-m \le (sum_{1,i}-sum_{2,i})-(sum_{1,j-1}-sum_{2,j-1}) \le m \]

这样的话就会出现同构式。我们又记 \(f_i=sum_{1,i}-sum_{2,i}\),那么原不等式等价于:

\[-m \le f_i-f_{j-1} \le m \]

由于我们考虑的是哪些 \(j\) 可以用于转移,所以我们得到关于 \(f_j\) 的不等式。

\[f_i-m \le f_j \le f_i+m \]

这样的话可以去的 \(j\)\(f_j\) 就是一个连续的区间了。我们要在这个区间里快速地找一个最小的 \(dp_j\)

想到线段树优化 dp。我们将 \(f_j\) 当做 \(j\) 在线段树中的下标。

那这样的话,首先我们线段树里 dp 值是 \(dp_0,dp_1,\cdots,dp_{i-1}\) 的。它们可以用来转移 \(dp_i\)

其次,对于线段树上的某个下标 \(pos\),可能很多个数的 \(f\) 都等于 \(pos\),但显然只有 \(dp\) 最小值可能更新后面的 \(dp\) 值。

所以我们只需要一个单修(取 \(\min\))区查 \(\min\) 的线段树即可用于优化 \(dp\)


以上说的都是一个宿舍里会同时膜拜 yyy 大牛和 c01 大牛的情况。如何转移一个宿舍膜拜同一个大牛的情况呢?

也很简单。直接动态维护某个数值相同区间里,当前 \(dp\) 的最小值即可。

代码:

P2418
#include<bits/stdc++.h>
#define int long long
#define ls (id<<1)
#define rs ((id<<1)|1)
using namespace std;

inline int read(){
	int x=0,f=1;char c=getchar();
	while(c<48){
		if(c=='-') f=-1;
		c=getchar();
	}
	while(c>47) x=(x<<1)+(x<<3)+(c^48),c=getchar();
	return x*f;
}

inline void write(int x){
	if(x<0) putchar('-'),x=-x;
	if(x<10) putchar(x+'0');
	else write(x/10),putchar(x%10+'0');
}

const int N=5e5+5;
const int inf=1e16;
int n,m,a[N],sum[2][N],f[N],dp[N];
//理论上来说,我们的区间长是[-n,n],所以线段树应该要开八倍空间
//sum[0][i],sum[1][i]:同sum1[i],sum2[i]。 
struct seg{
	int id,l,r,mn;
}tr[N<<3];

inline void pushup(int id){
	tr[id].mn=min(tr[ls].mn,tr[rs].mn);
}

inline void build(int id,int l,int r){
	tr[id].l=l,tr[id].r=r;
	if(l==r){
		tr[id].mn=inf;
		return ;
	}
	int mid=(l+r)>>1;
	build(ls,l,mid);build(rs,mid+1,r);
	pushup(id);
}

inline void update(int id,int pos,int k){
	if(tr[id].l==tr[id].r){
		tr[id].mn=min(tr[id].mn,k);
		return ;
	}
	int mid=(tr[id].l+tr[id].r)>>1;
	if(pos<=mid) update(ls,pos,k);
	else update(rs,pos,k);
	pushup(id);
}

inline int query(int id,int l,int r){
	if(l<=tr[id].l&&tr[id].r<=r){
		return tr[id].mn;
	}
	int mid=(tr[id].l+tr[id].r)>>1,ans=inf;
	if(l<=mid) ans=min(ans,query(ls,l,r));
	if(r>mid) ans=min(ans,query(rs,l,r));
	return ans;
}

signed main(){
	n=read(),m=read();
	//预处理各种数组 
	for(int i=1;i<=n;i++){
		a[i]=read();
		sum[0][i]=sum[0][i-1]+(a[i]==1);
		sum[1][i]=sum[1][i-1]+(a[i]==2);
		f[i]=sum[1][i]-sum[0][i];
		dp[i]=inf;
	}
	int mi=inf;
	build(1,-n,n);
	update(1,0,0);//这里一定要更新dp[0]=0,否则后面的最小值只能取到inf 
	for(int i=1;i<=n;i++){
		if(a[i]!=a[i-1]){
			//动态维护一段[j,i]区间的dp最小值 
			mi=dp[i-1];
		}
		int l=f[i]-m,r=f[i]+m;
		dp[i]=min(mi,query(1,l,r))+1;
		//i要么和同一个膜拜对象的一屋,要么和绝对值不超过m的一屋 
		update(1,f[i],dp[i]);//记得更新f[i]上的dp[i] 
		mi=min(mi,dp[i]);//维护区间最小dp值 
	}
	int ans=dp[n];
	printf("%lld",ans);
	return 0;
}
posted @ 2025-11-10 16:30  qwqSW  阅读(5)  评论(0)    收藏  举报