[dp单调队列+堆优化] poj3017 Cut the Sequence

link

题面 $

  给定一个数组 a, 要把它分成若干段,让每一段的和不超过 m m m ,求每一段的最大值之和最小是多少。

分析

  朴素的 d p dp dp 比较容易想到。
  定义 d p [ i ] dp[i] dp[i] 表示从 a [ 1 : i ] a[1:i] a[1:i] 分成若干段的 最小 最大值之和, i 0 i_0 i0 表示 s u m   a [ i 0 , i ] > m ,   s u m   a [ i 0 + 1 , i ] ≤ m sum \ a[i_0, i] > m, \ sum \ a[i_0 + 1, i] ≤ m sum a[i0,i]>m, sum a[i0+1,i]m 的那个临界值,有如下转移:
d p [ i ] = d p [ j ] + m a x   a [ j + 1 , i ]    ( i 0   ≤ j   < i ) dp[i] = dp[j] + max \ a[j+1, i] \ \ (i_0 \ ≤ j \ <i) dp[i]=dp[j]+max a[j+1,i]  (i0 j i)
  这样的转移是 O ( n 2 ) O(n^2) O(n2) 的。继续观察,发现这个 dp值一定是单调不减的,若假设 i 1 i_1 i1 a [ i 0 + 1 , i ] a[i_0 + 1, i] a[i0+1,i] 最大值的下标,我们可以得到对于 j < i 1 j <i_1 ji1 d p [ i ] = d p [ j ] + a [ i 1 ] dp[i] = dp[j] + a[i_1] dp[i]=dp[j]+a[i1],并且 j = i 0 j = i_0 j=i0 时一定能取得最小值。
  类似地,定义 i p + 1 i_{p+1} ip+1 a [ i p + 1 , i ] a[i_p + 1, i] a[ip+1,i] 最大值的下标,那么 i p ≤ j < i p + 1 i_p ≤ j <i_{p+1} ipjip+1 时, d p [ i ] = d p [ j ] + a [ i p + 1 ] dp[i] = dp[j] + a[i_{p+1}] dp[i]=dp[j]+a[ip+1],最小值在 i p i_p ip 取到。

  
  所以我们可以维护一个单调队列,然后遍历单调队列取得最小值。但是在最坏情况下,单调队列有O(n)个数,依次遍历的话复杂度还是 O ( n 2 ) O(n^2) O(n2)。因此可以考虑用堆去优化这个单调队列。
  在位置 i i i,若单调队列里有 k k k 个元素,单调队列里维护的是 i 1 , i 2 , i 3 . . . i i_1, i_2, i_3...i i1,i2,i3...i,最小值就是 d p [ i 0 ] + a [ i 1 ] , d p [ i 1 ] + a [ i 2 ] . . . d p [ i k − 1 ] + a [ i ] dp[i_0] + a[i_1], dp[i_1] + a[i_2]...dp[i_{k-1}] + a[i] dp[i0]+a[i1],dp[i1]+a[i2]...dp[ik1]+a[i] 的最小值。所以队首和队尾需要特殊处理一下,出队入队的元素也要处理一下,其实就是要维护好一个动态改变的最小堆。单调队列里的下标就像将一个需要遍历的区间拆成若干个点,减少了复杂度。
  
  具体细节参照代码。

#include<cstdio>
#include<queue>

#define here printf("modassing [%s] in LINE %d\n", __FUNCTION__, __LINE__);
#define debug(x) cout << #x << ":\t" << (x) << endl;

using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int, int> P;
const int maxn = 1e5 + 10;
const int maxm = 2e6 + 10;
const int INF = 0x3f3f3f3f;
const ll mod = 1e9 + 7;
//const double pi = acos(-1.0);
const double eps = 1e-11;

struct node
{
    ll val;
    int p;
    bool operator<(const node& m)  const     //按照小顶堆
    {
        return val > m.val;
    }
    node(ll k1, int k2){
        val = k1;
        p = k2;
    }
};

priority_queue<node> que;
int n, a[maxn], q[maxn], vis[maxn];
ll m, dp[maxn], val[maxn];

int main()
{
    scanf("%d %lld", &n, &m);
    for (int i = 1; i <= n; i++) scanf("%d", &a[i]);
    for (int i = 1; i <= n; i++)    //特判
    {
        if(a[i] > m)
        {
            printf("-1\n");
            return 0;
        }
    }

    int pos = 1, L = 1, R = 1;     //单调队列用一个数组维护,范围在[L,R], sum a[pos, i] <= m, sum a[pos-1, i] > m
    ll cur = a[1];                 //cur = a[pos, i]
    dp[1] = a[1];                   
    q[1] = 1;
    vis[1] = 1;                   //1 表示 表示这个位置在单调队列里
    que.push(node(a[1], 1));
    for (int i = 2; i <= n; i++)
    {
        cur += a[i];         //更新cur和pos值
        while(cur > m)
            cur -= a[pos++];
        while(L <= R && q[L] < pos)    //更新单调队列左端点
        {
            vis[q[L]] = 0;
            L++;
        }
        while(L <= R && a[q[R]] <= a[i])    //更新单调队列右端点
        {
            vis[q[R]] = 0;
            R--;
        }
        if(L <= R)         //如果单调队列里还有元素
        {
            val[q[L]] = dp[pos - 1] + a[q[L]]; 
            que.push(node(val[q[L]], q[L]));    //更新左部
            val[i] = dp[q[R]] + a[i];
            vis[i] = 1;
            que.push(node(val[i], i));   //由于在右部加入了位置i,进行更新
        }
        else                  //如果单调队列里没有元素,加入i
        {
            val[i] = dp[pos - 1] + a[i];
            vis[i] = 1;
            que.push(node(val[i], i));
        }
        q[++R] = i;
        while(vis[que.top().p] == 0 || que.top().val != val[que.top().p])   //如果已经不在单调队列里或者该值已经被更新
            que.pop();
        dp[i] = que.top().val;
    }
    printf("%lld\n", dp[n]);
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值