干草堆题解---单调队列优化DP

一.题面:

在这里插入图片描述
题意转化:将给定序列分成若干连续段,满足每一段的和都不大于前一段,求最大段数。

二.分析:

            我们考虑倒序去做,即求出每一段之和都大于等于前一段。这里有一个贪心的思路:我们考虑将最后一个单独作为第一段,然后倒序依次将元素累加,如果累加和大于前一段,就新开一段,最后输出段数即可。但是这样显然是错误的(虽然我一开始没想到为啥错 ),这里提供一组数据:

5
65
25
80
31
32

            如果按照贪心思路,那么我们的输出显然是 2 2 2,但是实际可以将 [ 31 , 32 ] [31,32] [31,32] 划为一段, [ 80 ] [80] [80] [ 65 , 25 ] [65,25] [65,25]分别划为一段,那么实际答案应该是 3 3 3

           我们考虑 D P DP DP。设 f [ i ] f[i] f[i] 表示从 n n n i i i 的序列最多划分成多少段。 但是在转移的过程中既要考虑 段数,也要考虑 在这种段数下的最后一段的和。会不会出现最优段数的情况下最后一段的和不成立,需要一个不是最优段数但最后一段和却成立的状态来转移呢? 需不需要多开一维同时存储这种高度下的宽度呢? 答案是否定的。 我们注意到一条性质: f [ i ] f[i] f[i] 最大时,也就是段数最多时,它所对应的最后一段的和一定是最小的。举个例子,比如如果 f [ 3 ] = 5 f[3] = 5 f[3]=5,那么从 3 3 3 n n n 这个序列被划分成五段时最后一段的和最小,如果划分成 4 4 4 段最后一段的很显然不会减小。因此 在段数达到最优的情况下,最后一段的和也对应最小(让转移更有可能性了)。我们就不需要考虑其它段数下的宽度的情况了。因此我们多开一个数组 g g g, 设 g [ i ] g[i] g[i] 表示 n n n i i i 的序列划分成 f [ i ] f[i] f[i] 段,最后一段的最小和。

           我们考虑转移:
            f [ i ] = m a x ( f [ j ] + 1 ) ( s u m [ i ] − s u m [ j ] ≥ g [ j ] ) f[i] = max(f[j]+1)(sum[i]-sum[j]\ge g[j]) f[i]=max(f[j]+1)(sum[i]sum[j]g[j])
           如果 f [ i ] < f [ j ] + 1 f[i]<f[j]+1 f[i]<f[j]+1 g [ i ] = s u m [ i ] − s u m [ j ] g[i]=sum[i]-sum[j] g[i]=sum[i]sum[j]
           如果 f [ i ] = f [ j ] + 1 f[i]=f[j]+1 f[i]=f[j]+1 g [ i ] = m i n ( g [ i ] , s u m [ i ] − s u m [ j ] ) g[i]=min(g[i],sum[i]-sum[j]) g[i]=min(g[i],sum[i]sum[j])
           特别的 f [ n + 1 ] = 0 f[n+1]=0 f[n+1]=0 g [ n + 1 ] = 0 g[n+1]=0 g[n+1]=0

           那么这样时间复杂度显然是 O ( n 2 ) O(n^2) O(n2),无法过掉这道题,考虑优化。我们观察到如果 j j j 能够满足条件转移 i i i,那么它很显然也可以转移给 i − 1 i-1 i1。那么它对 i − 1 i-1 i1的转移我们其实是不需要枚举的,我们实际上只需要让 f [ i − 1 ] f[i-1] f[i1] 的初值等于 f [ i ] f[i] f[i] g [ i − 1 ] g[i-1] g[i1] 的初值等于 g [ i ] + a [ i − 1 ] g[i]+a[i-1] g[i]+a[i1] ,就相当于把所有可以转移给 i i i j j j 都考虑了。(可以想一想这是为什么)。那么现在我们把所有 j ∈ [ i , n + 1 ] j∈[i,n+1] j[i,n+1] 分成两个集合,一个是可以转移给 i i i 的,一个是不能转移个 i i i 的,可以转移给 i i i,我们通过赋初值已经转移给了 i − 1 i-1 i1,那么我们考虑怎样处理不能转移给 i i i 的。
           如果我们用 set 维护这个集合,那么只需要让元素按照 转移难度 升序排列,然后从前往后判断是否可以转移转移,如果可以转移,我们将它转移给 i − 1 i-1 i1后弹出即可。最后把 i − 1 i-1 i1 加入 set,每个元素最多进 set 一次,出 set一次,时间复杂度 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n)
           至于怎样判断一个元素的 转移难度 呢,我们考虑 y y y x x x 转移条件是 s u m [ x ] − s u m [ y ] ≥ g [ y ] sum[x] -sum[y]\ge g[y] sum[x]sum[y]g[y],移项可得: s u m [ x ] ≥ s u m [ y ] + g [ y ] sum[x]\ge sum[y]+g[y] sum[x]sum[y]+g[y],而 x x x 变化的过程中 s u m [ y ] + g [ y ] sum[y]+g[y] sum[y]+g[y]是始终不变的,因此我们可以按照 s u m [ y ] + g [ y ] sum[y]+g[y] sum[y]+g[y]从小到大排序,越小转移就越容易。

           此时问题已经得到了解决,但是是否还存在更优秀的算法呢?
           我们注意到 f f f 数组是 从后往前 递增的,如果当前新加入元素它的转移难度也优于前面的元素,那么前面的元素不是就没有用了吗?
           基于这个想法,我们考虑 单调队列。我们对于当前的元素,从队首扫描并转移,出队。在队尾将所有转移难度比它更大的出队,最后将它加入队尾。这样,我们就维护了一个 决策点的转移难度单调递增,决策点的转移价值单调递增的单调队列。时间复杂度 O ( n ) O(n) O(n)
CODE:

#include<bits/stdc++.h>//高度最高时最底层一定最窄? 
using namespace std;// f[]数组单调递减, 并且如果i能满足g数组,那么 i - 1 也一定能满足 
const int N = 1e5 + 10;
int n, w[N], sum[N], res, f[N], g[N];
deque< int > q;// q维护决策集合   f不降, 能够成功的难度递增 
int main(){
	freopen("test.in", "r", stdin);
	freopen("test.out", "w", stdout);
	scanf("%d", &n);
	for(int i = 1; i <= n; i++) scanf("%d", &w[i]);
	for(int i = n; i >= 1; i--) sum[i] = sum[i + 1] + w[i];
    memset(g, 0x3f, sizeof(g));
    g[n + 1] = 0;
    q.push_back(n + 1);
    for(int i = n; i >= 1; i--){
    	f[i] = f[i + 1], g[i] = g[i + 1] + w[i];//i和 i + 1 拼在一起 
    	while(!q.empty() && (sum[i] - sum[q.front()] >= g[q.front()])){
    		if(f[q.front()] + 1 >= f[i]){
    			f[i] = f[q.front()] + 1;
    			g[i] = min(g[i], sum[i] - sum[q.front()]);
			}
    		q.pop_front();
		}
        while(!q.empty() && ((g[i] + sum[i] - sum[q.back()]) <= g[q.back()])) q.pop_back();
        q.push_back(i);
	}
	cout << f[1] << endl;
	return 0;
}
/*
7
1
26
72
17
61
67
20


12
86
53
73
69
89
23
27
65
29  
89  
49    
63  

*/

三.总结

         单调队列 优化DP是很套路的技巧,我们需要考虑 一个元素新加入后会不会有前面的元素用不到 。如果满足了这个性质,我们就可以考虑使用单调队列来进行优化。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值