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 做法
考虑优化刚才的思路。我们发现转移式很简单,最麻烦的在于这个判定式。于是我们考虑化简判定式。
先看一下原本的式子:
我们考虑将 \(i\) 和 \(j-1\) 分开:
这样的话就会出现同构式。我们又记 \(f_i=sum_{1,i}-sum_{2,i}\),那么原不等式等价于:
由于我们考虑的是哪些 \(j\) 可以用于转移,所以我们得到关于 \(f_j\) 的不等式。
这样的话可以去的 \(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;
}

浙公网安备 33010602011771号