基础数据结构----单调队列与最大子序和问题

下面介绍单调队列的经典应用----最大子序和问题。

那么,什么是子序和呢?给定长度为n的整数序列A,他的“子序列”定义为A中非空的一段连续的元素。例如,序列(6,-1,5,4,7)中前4个元素的子序和为6 + (-1) + 5 + 4 = 14。

最大子序列和问题按子序列有无长度限制分为两种:

        问题(1):不限制子序列的长度。在所有可能的子序列中找到一个子序列,该子序列和最大。

        问题(2):限制子序列的长度。给定一个限制长度为m,找出一段长度不超过m的连续子序列,使它的子序和最大。

对于问题(1),我们以http://acm.hdu.edu.cn/中的1003题为例:

Max sum(hdu 1003)

问题描述:给定一个序列,求最大子序列和。

输入:第1行输入整数T,表示测试用例个数,1 <= T <= 20;后面T行中,每行第1个数输入N,后面输入N个数,1 <= N <= 100000,每个数在 [ -1000, 1000 ] 区间内。

输出:每个测试输出两行。第一行是 "Case #: ",其中 "#" 表示测试序列号;第二行输出3个数,第1个数是最大子序和,第2个和第3个数是开始和终止位置。两个测试点之间输出一个空行。

解法一:

贪心法。逐个扫描序列中的元素并累加。加到一个正数时,子序和会增加;加到一个负数时,子序和会减少。如果当前得到的和变成了负数,这个负数在接下来的累加中会减少后面的求和,所以抛弃它,从下一个位置重新开始求和。时间复杂度为O(n)

#include<bits/stdc++.h>
using namespace std;

const int INF = 0x7fffffff;

int main()
{
	int t;
	cin >> t;//输入测试用例数
	
	for(int i = 1; i <= t; i++)
	{
		int n;
		cin >> n;
		
		int maxsum = -INF;//最大值,初始化为一个极小值
		int start = 1,end = 1,p = 1;//起点、终点,扫描位置
		int sum = 0;
		
		for(int j = 1; j <= n; j++)
		{
			int a;
			cin >> a;
			sum += a;//读入一个元素,累加
			if(sum > maxsum)
			{
				maxsum = sum;
				start = p;
				end = j;	
			}
			if(sum < 0)
			{//扫描到j时,若前面的最大子序和是负数,从下一个重新开始求和 
				sum = 0;
				p = j + 1;//初始化扫描位置 
			}	
		}
		
		cout << "Case " << i << ":" << endl;
		cout << maxsum << " " << start << " " << end << endl;
		if(i != t)
		{
			cout << endl;
		}	
	}
	
	return 0; 
}

 解法二:

动态规划。定义状态dp[ i ],表示以a[ i ]为结尾的最大子序和。dp[ i ]的计算有两种情况:

        (1)dp[ i ]只包括一个元素,就是a[ i ];

        (2)dp[ i ]包括多个元素,从前面某个a[ v ]开始,v < i,到a[ i ]结束,即dp[ i - 1] + a[ i ]。

取两者的最大值,得到状态转移方程dp[ i ] = max(dp[i - 1] + a[ i ], a[ i ])。在所有的dp[ i ]中,最大值就是题目的解。时间复杂度为O(n)

#include<bits/stdc++.h>
using namespace std;

int dp[100005];//dp[i]:以第i个数为结尾的最大值

int main()
{
	int t;
	cin >> t;
	for(int k = 1; k <= t; k++)
	{
		int n;
		cin >> n;
		for(int i = 1; i <= n; i++)
		{
			cin >> dp[i];//用dp[]存储数据a[]	
		}
		
		int start = 1,end = 1,p = 1;//起点、终点,扫描位置
		int maxsum = dp[1];
		for(int i = 2; i <= n; i++)
		{
			if(dp[i-1] + dp[i] >= dp[i])
			{
				dp[i] = dp[i-1] + dp[i];//dp[i-1] + a[i]比a[i]大	
			}
			else
			{
				p = i;//若a[i]更大,那么dp[i]就是a[i] 
			}
			
			if(dp[i] > maxsum)
			{//dp[i]是一个更大的子序和 
				maxsum = dp[i];
				start = p;//以p开始 
				end = i;//以i结尾	
			}	
		}
		cout << "Case " << k << ":" << endl;
		cout << maxsum << " " << start << " " << end << endl;
		if(k != t)
		{
			cout << endl;
		}	
	}
    
    return 0;	
} 

下面是问题(2)的求解:

这题和上一个博文中的“滑动窗口”类似,可以用单调队列的“窗口、删头、去尾”解决问题(2)

首先求前缀和s[ i ]。s[ i ]是a[ 1 ] ~ a[ i ]的和,计算所有的s[ i ] ~ s[ n ],时间复杂度为O(n)

求完前缀和后,问题(2)实际上转化为:找出两个位置 i、k,使s[ i ] - s[ k ]最大,i - k <= m。

我们来思考用DP求解,把问题进一步转化为:首先固定一个 i,找到它左边的一个端点 k,i - k <= m,使s[ i ] - s[ k ]最大,定义这个最大值为dp[ i ];逐步扩大 i,求得所有的dp[ i ],其中的最大值就是问题的求解。如果简单地暴力检查,对每个 i 检查比它小的 m 个s[ k ],那么总的时间复杂度为O(mn),会超时

暴力检查的方法不可行,改用一个大小为 m 的窗口寻找最大子序和 ans。从头到尾依次把 s[ ] 的元素放入这个窗口。

        (1)首先把 s[ 1 ] 放入窗口,并且记录 ans 的初始值 s[ 1 ]。

        (2)接着把 s[ 2 ]放入窗口(假设窗口长度大于2),有两种情况:如果 s[ 1 ] <= s[ 2 ],那么更新 ans = max{ s[ 1 ],s[ 2 ],s[ 2 ] - s[ 1 ] };如果 s[ 1 ] > s[ 2 ],那么保持 ans = s[ 1 ]不变,然后从窗口中抛弃 s[ 1 ],只留下 s[ 2 ],因为后面再把新的 s[ i ’ ] - s[ 2 ] 比 s[ i ’ ] - s[ 1 ]更大(注意s为前缀和,s[ 1 ] > s[ 2] 实际上是 a[ 2 ]一定小于0,留下s[ 2 ]是为了跳过负数 a[ 2 ])

继续这个过程,直到所有的 s[ ] 处理结束

总结上面的思路,把新的 s[ i ] 放入窗口时:

        (1)把窗口内比 s[ i ]大的所有 s[ j ]都抛弃,i - j <= m,因为这些 s[ j ]在处理 s[ i ] 后面的 s[ i ’ ] 时用不到了,s[ i ’ ] - s[ i ] 要优于 s[ i ’ ] - s[ j ],保留 s[ i ] 就可以了;

        (2)若窗口内最小的是 s[ k ],此时肯定有 s[ k ] < s[ i ],检查 s[ i ] - s[ k ]是否为当前的最大子序列和,如果是,就更新最大子序列和ans;

        (3)每个 s[ i ] 都会进入队列。

此时,最优策略是一个“位置递增、前缀和也递增”的序列,用单调队列最合适。s[ i ]进入队尾时,如果原队尾比 s[ i ] 大,则去尾;如果队头超过窗口 m,则去头,而最小的那个 s[ k ] 就是队头。算法跟“滑动窗口”差不多。

在这个单调队列中,每个 s[ i ] 只进出队列一次,计算复杂度为O(n)

#include<bits/stdc++.h>
using namespace std;

deque<int> dq;
int s[1000005];

int main()
{
	int n,m;
	cin >> n >> m;	
	for(int i = 1; i <= n; i++)
	{
		cin >> s[i];
	}
	
	for(int i = 1; i <= n; i++)
	{//计算前缀和 
		s[i] = s[i] + s[i-1];
	}
	
	int ans = -1e8;
	dq.push_back(0);
	for(int i = 1; i <= n; i++)
	{//同理,队列中存入下标 
		while(!dq.empty() && dq.front() < i - m)
		{
			dq.pop_front();//队头超过m的范围:删头 
		}
		
		if(dq.empty())
		{
			ans = max(ans,s[i]);
		}
		else
		{
			ans = max(ans,s[i] - s[dq.front()]);//队头就是最小的s[k] 
		}
		
		while(!dq.empty() && s[dq.back()] >= s[i])
		{
			dq.pop_back();//队尾大于s[i],去尾	
		} 
		dq.push_back(i);
	}
	cout << ans << endl;
	
	return 0;
}

  • 6
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Valueyou24

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值