CF 833B(动态规划优化DP)

  • 题目大意

    一个长度为n的序列 a[i] ,让你把它分成k份,将每份中不同的数的个数求和,问这个最大的和是多少.
    1n35000,1k50

  • 分析

    看见这道题我们很容易想到从动态规划的角度思考。
    这道题类似于n个乘号,题目中说分成k份,那么我们可以通过k-1份的状态来得到份数是k的时候的状态.
    d[k][i] 表示前i个数分成k份得到的答案,那么有方程:

    dp[k][i]=max{dp[k1][j1]+dif[j][i]},(kji)dif[l][r]lr

    这样算法的复杂度是 n2k(dif[l][r]) ,肯定超时。
    状态的数目的复杂度是确定的 nk ,需要考虑的是怎么优化状态的转移.
    这道题不能将 dif 数组单独拿出来 进行维护,这样复杂度很高(好像主席树可以实现)
    根据题目的数据我们可以大致确定状态的转移是 O(1) O(logn) 的复杂度.
    优化状态转移的一个很重要的思想就是:信息重用

    整个动归的过程有三层循环,最外层是k,中间层遍历的是 i ,对于一个给定的i,再用 j 从下标k遍历到 i .这个过程怎么信息重用呢,也就是哪些信息可以为后面的状态提供帮助呢?
    通过观察状态转移方程,遍历的过程中dif的求解是有一定规律的,有两个思路:

    1.定j移动i,先求解 dif[j][1...i] ,然后求解 dif[j][i+1]

    2.定i移动j,我们先求得 dif[1...j][i] ,然后求解 dif[j+1][i]

    对于第一种思路,在i遍历的过程中,对于一个新的 i ,我们思考能不能由前面的dif[j][1...i]通过某种构造得到 dif[j][i+1] ,其实不难发现要求 dif[j][i+1] 好像只和 dif[j][i] 有关,所以我们思考怎么通过 dif[j][i] 的信息来得到 dif[j][i+1]

    知道了 [j,i] 区间中不同元素的个数,求 [j,i+1] 区间中不同元素的个数,那么就是看 a[i+1] 是否在区间 [j,i] 中,容易想到用一个数组 pre[x] 记录在下标 x 之前离a[x]最近的数的下标,这样就可以 O(1) 的复杂度从 dif[j][i] 推得 dif[j][i+1] .

    这样之后算法复杂度降到了 O(n2k) ,但还是过不了

    接下来观察第二种情况,在定 i 移动j的过程中,我们要求的是 max{dp[k1][j1]+dif[j][i]},(kji)
    现在我们思考是否有快速的方法求出这个最大值,
    一般来说,维护一个区间的最大值有两种思路:

    1.用一个变量来动态更新这个最大值,这要求这个对这个区间最大值的询问符合一定的顺序以使得在遍历的过程中(遍历的过程也是询问的过程)始终能够以 O(1) 的复杂度来更新最大值
    2.如果我们遍历(询问)的顺序使得我们不能够在 O(1) 的复杂度去更新最大值,也许可以用线段树或树状数组之类的数据结构来维护这一信息。

    如果这道题方程中只是求 max{dp[k1][j1]},(kji) 那么很明显的我们可以用思路一来做。
    但加入了 dif[j][i] 这一项之后不能只在遍历 i 的过程中记录下最大的值。所以要想在O(1)的复杂度内求出状态的转移是不太可能的,考虑思路二:

    我们不妨简化一下这个式子以便于思考:
    我们令 C[j]=dp[k1][j1]+dif[j][i] ,这样我们就是去求一个 max{C[j]}(kji)

    思考i在变化的时候 C[j](kji) 将怎么变化?变 i 化的时候dp[k1][j1]是不会变化的,变化的只是 dif[j][i] ,那么能否有某种快速的方法去更新这种变化呢,前面已经说过一种通过 O(1) 的复杂度从 dif[j][i] 转移到 dif[j][i+1] 的方法。

    类似的道理,我们可以发现第i个数只对区间 [x,i],x[pre[i]+1,i] dif 值造成影响,这个影响是相对于 i1 的情况而言的,我们可以得到转移式: dif[x][i]=dif[x][i1]+1,x[pre[i]+1,i]

    由这个更新式我们可以看出这是一个区间的更新,所以我们可以通过线段树的区间更新来实现,又由于 C[j] 表达式中对于一个给定的 j dp[k1][j1]的值和 i 是没有关系的,所以我们可以将dp数组的值作为线段数 C[j] 的初始值

  • 代码

#include<cstdio>
#include<iostream>
#include<cmath>
#include<cstring>
#include<cstdlib>
#include<queue>
#include<map>
#include<algorithm>
#include<set>
#include<stack>
using namespace std;
const  int MAXN=35005;
const int MAXK=55;
int n,k;
int a[MAXN];
int tree[MAXN*4];
int lazy[MAXN*4];
int dp[MAXK][MAXN];//dp[i][j]表示前j个数分成i份的答案
int pre[MAXN];//表示和下标相同的前一个数的下标
int pos[MAXN];//pos[x]在输入的过程中保存数x的下标
void PushUp(int rt)
{
     tree[rt]=max(tree[rt*2],tree[rt*2+1]);
}
void PushDown(int rt)
{
    tree[rt*2]+=lazy[rt];
    tree[rt*2+1]+=lazy[rt];
    lazy[rt*2]+=lazy[rt];
    lazy[rt*2+1]+=lazy[rt];
    lazy[rt]=0;
    return ;
}
void Build(int pos,int l,int r,int rt)
{
    lazy[rt]=0;
    if(l==r)
    {
        tree[rt]=dp[pos][l-1];
        return ;
    }
    int m=(l+r)/2;
    Build(pos,l,m,rt*2);
    Build(pos,m+1,r,rt*2+1);
    PushUp(rt);
}
void Update(int L,int R,int l,int r,int rt)//更新区间[L,R],[l,r]为当前线段所在区间
{
    if(L<=l && R>=r)
    {
        lazy[rt]++;
        tree[rt]++;
        return ;
    }
    PushDown(rt);
    int m=(l+r)/2;
    if(L<=m)Update(L,R,l,m,rt*2);
    if(R>m)Update(L,R,m+1,r,rt*2+1);
    PushUp(rt);
}
int Query(int L,int R,int l,int r,int rt)
{
      if(L<=l && R>=r)
      {
            return tree[rt];
      }
      int ans=0;
      PushDown(rt);
      int m=(l+r)/2;
      if(L<=m)ans=max(ans,Query(L,R,l,m,rt*2));
      if(R>m)ans=max(ans,Query(L,R,m+1,r,rt*2+1));
      PushUp(rt);
      return ans;
}
void Work()
{
    for(int i=1;i<=k;i++)
    {
        Build(i-1,1,n,1);
        for(int j=i;j<=n;j++)
        {
             Update(pre[j]+1,j,1,n,1);
             dp[i][j]=Query(i,j,1,n,1);
        }
    }
    cout<<dp[k][n]<<endl;
}
int main()
{
    while(scanf("%d%d",&n,&k)!=EOF)
    {
         memset(pre,0,sizeof(pre));
         memset(pos,0,sizeof(pos));
         memset(tree,0,sizeof(tree));
         memset(dp,0,sizeof(dp));
         for(int i=1;i<=n;i++)
         {
             scanf("%d",&a[i]);
             pre[i]=pos[a[i]];
             pos[a[i]]=i;
         }
         Work();
    }
    return 0;
}
/*
4 1
1 2 2 1
*/
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值