单调队列优化DP能到什么程度(今天中午进行的实验记录)

时间:2021-02-12 17:37:03

想了一早上单调队列优化DP,总觉得不能优化到哪里去,又从来没有做过这种需要用单调队列优化的DP,于是自己用手模拟了一下实现过程,瞬间就明白了单调队列优化DP,这个DP的转移方程应该具有的性质。

单调队列很好理解,就是一个双向队列,队首队尾允许删除操作,队尾进行添加操作,维护整个队列的严格单调性,即队列中不存在相等的元素(这样时间常数小一点)。

那么用单调队列优化的DP应该具有怎样的性质呢?

假如我们有下面的DP转移方程:

    f[i] = min( f[j] ) + a[i]

那么当 j 满足一个条件: Low[i] <= j <= Up[i] ,这里的 Low 和 Up 是关于 i 的单调函数,而且是单调递增的,为什么呢?联系经典的单调队列入门题: Sliding Window 想想就清楚了: 当我用下一个 Low[i] 的时候,Low[i] 必须大于等于 Low[i-1] ,因为队首涉及到了要出队列的操作,而队尾的元素上界: Up 也是必须具有单调递增的性质,因为再用队尾的元素的时候,涉及到添加元素的操作。如果还是不很明白,那么联系 Sliding Window 仔细想想这个单调队列删除插入的过程即可。很好想通的。

那么单调队列到底能优化到怎么样的程度呢?我们来看看下面的实验:

【实验方程】:

f[i] = min ( f[j] ) + a[i] ( 1<= j <= i-1 )

【实验过程】:

我做了个测试数据,不大,刚好有 10^5 个数。用传统的 o( n^2 ) 的算法铁定超时,二这个转移方程来看,我们可以不用单调队列优化到 o ( n ) 的复杂度:就是记录一个 1——i 的最小值 Min 每次计算 f[i] 的时候用 Min + a[i] 即可。

那么这道题总共就可以写出三个程序,时间复杂度分别是;

传统的二重循环判定: o( n^2 )

用最小值优化:o( n )

用单调队列优化:未知 (这就是这个实验要探讨的内容)

下面先给出实验程序:

1、传统的二重循环判定:

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<ctime>
#include<cstdlib>
#define INF 0x7fffffff
using namespace std;
int main()
{
freopen("in.in","r",stdin);
freopen("out.out","w",stdout);
int n,a[100005],f[100005];
scanf("%d",&n);
for(int i=1;i<=n;i++)
scanf("%d",&a[i]);
f[1]=a[1];
for(int i=2;i<=n;i++)
{
int Min=INF;
for(int j=1;j<i;j++)
if(Min>f[j]) Min=f[j];
f[i]=Min+a[i];
}
for(int i=1;i<=n;i++)
{
printf("%d",f[i]);
if(i==n) printf("\n");
else printf(" ");
}
return 0;
}


用最小值优化:

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<ctime>
#include<cstdlib>
#define INF 0x7fffffff
using namespace std;
int main()
{
freopen("in.in","r",stdin);
freopen("out.out","w",stdout);
int n,a[100005],f[100005];
scanf("%d",&n);
for(int i=1;i<=n;i++)
scanf("%d",&a[i]);
f[1]=a[1];
int Min=f[1];
for(int i=2;i<=n;i++)
{
f[i]=Min+a[i];
if(f[i]<Min) Min=f[i];
}
for(int i=1;i<=n;i++)
{
printf("%d",f[i]);
if(i==n) printf("\n");
else printf(" ");
}
return 0;
}


用单调队列优化:

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<ctime>
#include<cstdlib>
#define INF 0x7fffffff
using namespace std;
int main()
{
freopen("in.in","r",stdin);
freopen("out.out","w",stdout);
int n,a[100005],f[100005];
int q[200000],head,tail;
scanf("%d",&n);
for(int i=1;i<=n;i++)
scanf("%d",&a[i]);
f[1]=a[1];
tail=1; head=0; q[1]=1;
for(int i=2;i<=n;i++)
{
f[i]=f[q[1]]+a[i];
//printf("f[q[1]]=%d\n",f[q[1]]);
//printf("f[%d]=%d\n",i,f[i]);
while(f[i]<=f[q[tail]] && 1<=tail) tail--;
q[++tail]=i;
}
for(int i=1;i<=n;i++)
{
printf("%d",f[i]);
if(i==n) printf("\n");
else printf(" ");
}
return 0;
}


程序有了,我们就可以做实验了:

我先用了一个 10^4 的数据规模来测试这三个程序,下面我们看看他们跑的时间:

单调队列优化DP能到什么程度(今天中午进行的实验记录)

从上面的数据结果可以看出,使用赤果果的的 二重循环 的话,消耗的时间几乎是使用 o( n )算法的 60 倍,然而使用 单调队列 优化的方法时,时间竟然和我预想的 “和赤果果的 o( n^2 ) 差不多”有很大的差别,竟然和 o( n ) 几乎一样了!于是不信邪的我又试了一组数据,这下我把数据规模弄到了 10^5 这个级别,下面是实验结果:

单调队列优化DP能到什么程度(今天中午进行的实验记录)

我想,到了这个地步,单调队列的时间复杂度我们不用质疑了,相当于去掉了一重循环,碉堡有木有!!

所以以后能用单调队列就尽量用单调队列吧