算法学习:单调队列(dp优化)

单调队列是什么:

单调队列,顾名思义,一个是单调一个是队列。首先是单调,可以是单调递增单调递减,也可以是某种特殊数据的组合的比较方式,是可以自定义>号的这种(我想应该可以写成运算符重载,或者函数引用,不过我还不会qwq)。其次是队列,队列其实就是封装好的,只可以从队首队尾进行操作的数据结构。不过由于队首队尾都需要推入和弹出操作,所以用std::deque很适合。

单调队列如何能优化dp过程:

思考这样一个问题,给定一组数,从a0一直到an-1,需要求某段连续区间 [l, r]内的最值,其中r-l==k固定,应当怎么求。

可能用stl的multiset很好求,时间复杂度为O(nlogn),其实这个复杂度也是可以接受的。但是单调队列能够在O(n)的复杂度求出答案。

以求最大值为例,我们设有一个优先队列deque<pair<int,int>>dq,第一个数表示位置,第二个数表示数值大小。优先队列内部相邻的两个元素i和j(i更靠近队首)满足条件:i.first < j.first && i.second > j.second

假设我们已经考虑到了[0,k-1]的范围,现在需要考虑a[k]同时需要舍弃a[0],再假设a[i]为[0,k-1]内最大值。1.·我们需要先把队尾的所有的值(也就是second)比a[i]小的弹出,然后在推入(i,a[i])。2.我们需要看队首的元素的坐标(也就是first)是否是0(即是即将被舍弃元素的坐标),若是,则弹出。

上述过程大概就是单调队列维护的过程。在这个过程中,我们会发现,如果i,j两个坐标,i<j而且a[i]<a[j],我们发现在舍弃i时,对区间最大值完全没影响的,如果改成a[i]>a[j],则很有可能在i被舍弃后,a[j]成为最大的值。

总而言之,单调队列能够在O(n)内维护区间最大值。

题目

例题1:P2698 [USACO12MAR] Flowerpot S - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

题目翻译和简介洛谷已经给了,直接说思路。

需要维护最大最小的两个单调队列,而且一旦满足极值差的要求,需要左侧指针的移动。

然后从左到右遍历一遍就好,单调队列复杂度O(n)

ac代码

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

/*以维护区间最大值的递增序列为例,区间整体是向右侧移动的。
队首是区间最大值,队尾是相对的最小值。如果说新的点,大于所有区间内的最大值,则队列清空,只剩这个点
如果说新的点小于区间内最大值,则从队尾查起,如果有比它小的,都弹出。
这样的功能是什么,是能够保证队列从队首到队尾,依次递减。
*/
int ab(int x)
{
	if (x < 0)
		return -x;
	else
		return x;
}
struct node
{
	int x, y;
};
int main(void)
{
	std::ios::sync_with_stdio(false);
	cin.tie(nullptr);

	int n, d;
	cin >> n >> d;
	vector<struct node> a(n);
	for (int i = 0; i < n; i++)
	{
		cin >> a[i].x >> a[i].y;
	}
	sort(a.begin(), a.end(), [&](struct node i, struct node j)
		 { return i.x < j.x; });//按照坐标排序

	int l, r, ans = -1;
	l = r = a[0].x;//最初从最左侧雨滴的坐标开始

	deque<struct node> dq_mx, dq_mn;//单调队列,分别维护当前区间的最大值和最小值
	dq_mx.push_back(a[0]), dq_mn.push_back(a[0]);

	for (int i = 1; i < n; i++)
	{
		while (!dq_mx.empty() && dq_mx.back().y <= a[i].y)
		{
			dq_mx.pop_back();
		}
		dq_mx.push_back(a[i]);//队尾检查
		while (!dq_mn.empty() && dq_mn.back().y > a[i].y)
		{
			dq_mn.pop_back();
		}
		dq_mn.push_back(a[i]);

		if (dq_mx.front().y - dq_mn.front().y >= d)
		{
			int temp = ab(dq_mx.front().x - dq_mn.front().x);
			if (ans == -1)
			{
				ans = temp;
			}
			else
				ans = min(ans, temp);

			temp = min(dq_mx.front().x, dq_mn.front().x);
			while (!dq_mx.empty() && temp == dq_mx.front().x)
			{
				dq_mx.pop_front();
			}
			while (!dq_mn.empty() && temp == dq_mn.front().x)
			{
				dq_mn.pop_front();
			}//队首检查,看需不需要把队首踢出去
		}
	}

	cout << ans << "\n";
}

例题2:Problem - E - Codeforces

题目简介(翻译):输入给定四个正整数n,m,k,d。有一条河,河流宽度为m,河岸宽度为n,形成一个n*m的格子区域,每个格子有对应的水深(两岸水深为0)。

建一座桥,桥有立柱,要求相邻立柱之间的距离不能超过d,且最左和最右侧必须加立柱,加一个立柱的代价是a[i][j]+1(a[i][j]为水深),需要得到建桥的最小代价。

最后题目又套了一层,需要连续建k个桥(当然前面的问题能解决了后面的就很轻松了)

(第2行截面图,黑色表示桥的立柱)

(图来自codeforce)

思路:比如我们即将考虑到此行的第i个位置,dp[i]表示在i处建立柱,且只考虑i之前位置的最小值。那么我们需要和后d个数中最小的进行状态转移,也就是dp[i]=dp[j]+cost(代价)。如果说对后面每一个数都去考虑,则复杂度为O(md),其实就是O(m2)的复杂度,绝对会tle。

所以只需要单调队列去维护后d个数字的最小值就可以了。复杂度为O(n)

ac代码

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

const ll MOD = 998244353;

int n, m, k, d;

ll judge(vector<ll> &num) // 对每一行都判别,注意加引用,否则会无故增加时间
{
    ll siz = num.size();
    vector<ll> dp(siz, 0);

    dp[0] = 1;
    ll l = 0, r = 0;

    deque<pair<ll, ll>> q;           // 单调队列
    q.push_back(pair<ll, ll>(0, 1)); // first表示坐标,second表示代价
    for (ll i = 1; i < siz; i++)
    {
        r = i;
        l = max(l, r - d - 1);
        while (!q.empty() && q.front().first < l)
        {
            q.pop_front();
        }
        dp[r] = q.front().second + num[r];

        while (!q.empty() && dp[r] < q.back().second)
        {
            q.pop_back();
        }
        q.push_back(pair<ll, ll>(r, dp[r]));
    }

    return dp[siz - 1];
}
void slv(void)
{
    cin >> n >> m >> k >> d;
    vector<vector<ll>> a(n, vector<ll>(m));

    vector<ll> res;
    for (int i = 0; i < n; i++)
    {
        for (int j = 0; j < m; j++)
        {
            cin >> a[i][j];
            a[i][j]++; // 因为要+1,先加上方便些
        }

        res.push_back(judge(a[i]));
    }

    ll ans = 0;
    for (int i = 0; i < k; i++)
        ans += res[i];
    ll temp = ans;
    for (int i = k; i < n; i++)
    {
        temp -= res[i - k];
        temp += res[i];
        ans = min(temp, ans);
    }
    cout << ans << "\n";
}
int main(void)
{
    std::ios::sync_with_stdio(false);
    cin.tie(NULL);
    int t;
    cin >> t;
    while (t--)
    {
        slv();
    }
}

  • 26
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值