优化dp
背景
有些时候dp的时间复杂度和空间复杂度很高不符合题目要求,这时候我们就会想尽一切办法去卡常优化.下面我就来学习(复习)一下dp的优化
正题
状态压缩DP
背景
我们来看看这道题目P1879 玉米田Corn Fields
这道题的状态有很多种普通的搜索完全做不到.
前置知识
位运算想必大家都学过,但有些同学没学,所以我在这简单阐述一下.
| 按位或 把一个数拆成二进制 把每一位进行或的运算返回十进制的值如2(10)|1(01)=3(11)
& 按位或 把一个数拆成二进制 把每一位进行与的运算返回十进制的值如5(101)&3(011)=1(001)
^ 按位异或 把一个数拆成二进制 相同位置不同为1相同为0 5(101)^1(001)=4(100)
>> 右移 如x>>y相当于x/(2^y)向下取整 9(1001)>>1=4(100)
<< 左移 如x<<y相当于x*2^y 5(101)<<1=10(1010) 2(10)<<2=8(1000)
使用
依然是那道题
我们用普通dp试试
这个时间复杂度可是
难不成你能降一维?你以为单调队列了不起啊.
没错单调队列就是可以为所欲为!
打完暴力DP代码我们发现大部分的时间都浪费在寻找(i-k+1~i)中的最小值上.
找最小值我们思考一下发现堆似乎能解决这个问题可是我要求的数据范围是n<=10^8
堆虽然比线段树的解法要更优秀但是还是很难过我的毒瘤数据.
别忘了我们刚刚学了单调队列所以这道题目就可做了
每次加入a[i],当i>=k时取出队首然后输出假设队首的位置不在区间内就弹出
(就好像你太老了必须退役)
这样我们就能愉快的写代码了~
#include <bits/stdc++.h>
using namespace std;
int num[1000001],p[1000001],a[1000001];
int k,head,tail,n;
void dpmin()
{
int i;
head=0;
tail=1;
for(i=1;i<=n;i++)
{
while(num[head]<=i-k&&head<=tail) ++head;
while(a[i]<=p[tail]&&head<=tail) --tail;
p[++tail]=a[i];
num[tail]=i;
if(i>=k) printf("%d ",p[head]);
}
printf("\n");
}
void dpmax()
{
int i;
head=0;
tail=1;
for(i=1;i<=n;i++)
{
while(num[head]<=i-k&&head<=tail) ++head;
while(a[i]>=p[tail]&&head<=tail) --tail;
p[++tail]=a[i];
num[tail]=i;
if(i>=k) printf("%d ",p[head]);
}
}
int main()
{
int i;
scanf("%d%d",&n,&k);
for(i=1;i<=n;i++)
scanf("%d",&a[i]);
dpmin();
dpmax();
}
读者:好吧,我承认有时候单调队列可以为所欲为,但是为什么它是
因为每个元素都只进出队列最多一次所以是
你如果还不承认单调队列的强大的话那么我们进入一个毒瘤题目
提升训练
毒瘤题目
为了找这个题目我找了极其之久
读者:这个题目看起来什么都不像啊,数据范围又这么毒瘤.
如果我们考虑暴力的话那么时间复杂度是
现在我们定义
(由于是个环所以要开两倍空间)
看出来了吗?
我们只要求
所以我么可以维护一个单调递增的单调队列来维护最小的
这是顺时针之后再反过来跑一遍就可以了
#include <bits/stdc++.h>
using namespace std;
long long oil[2000001];
long long dis[2000001];
long long sum[2000001];
long long b[2000001];
bool ok[2000001];
struct
{
long long data;
int no;
}que1[2000001],que2[2000001];
int main()
{
int n,i,head1=1,tail1=0,head2=1,tail2=0;
scanf("%d",&n);
for(i=1;i<=n;i++)
{
sdpcanf("%lld%lld",&oil[i],&dis[i]);
oil[i+n]=oil[i];
dis[i+n]=dis[i];
b[i]=oil[i]-dis[i];
b[i+n]=b[i];
}
for(i=1;i<=n*2;i++)
sum[i]=sum[i-1]+b[i];
// sum[1]=0;
for(i=1;i<=n;i++)
{
while(head1<=tail1&&sum[i]<=que1[tail1].data) tail1--;
que1[++tail1].data=sum[i];
que1[tail1].no=i;
}
for(i=1;i<=n;i++)
{
// while(que1[head1].no<i&&head1<=tail1) head1--;
while(head1<=tail1&&sum[i+n-1]<=que1[tail1].data) tail1--;
que1[++tail1].data=sum[i+n-1];
que1[tail1].no=i+n-1;
if(que1[head1].no==i-1) head1++;
if(que1[head1].data>=sum[i-1]) ok[i]=1;
}
memset(sum,0,sizeof(sum));
b[1]=b[1+n]=oil[1]-dis[n];
for(i=2;i<=n;i++)
b[i]=b[i+n]=oil[i]-dis[i-1];
sum[0]=0;
for(i=1;i<=n*2;i++)
sum[i]=b[i]+sum[i-1];
// sum[1]=0;
for(i=0;i<n;i++)
{
while(head2<=tail2&&sum[i]>=que2[tail2].data) tail2--;
que2[++tail2].data=sum[i];
que2[tail2].no=i;
}
for(i=1;i<=n;i++)
{
// while(head2<=tail2&&que2[head2].no<i) head2--;
while(head2<=tail2&&sum[i-1+n]>=que2[tail2].data) tail2--;
que2[++tail2].data=sum[i-1+n];
que2[tail2].no=i-1+n;
// printf("%lld ",que2[head2].data);
if(que2[head2].no==i-1) head2++;
if(que2[headxinl2].data<=sum[i+n]) ok[i]=1;
}
for(i=1;i<=n;i++)
{
if(ok[i]) printf("TAK\n");
else printf("NIE\n");
}
}
我调了1天QAQ
一些习题
想要学好一种算法当然要练习啦!
跳房子
牛的阵容Cow Lineup
挤奶牛Crowded Cows
其实你在洛谷里面搜一下单调队列的算法标签就有很多题啦!
总结
我们发现这个单调队列我们只能对含有
所以我在这承认单调队列确实不能为所欲为
虽然有时候它非常的快但有时候会被卡常如Bzoj1531他的速度因为常数问题比状态压缩dp慢的多!
最后的总结
这些优化都非常难以调试所以我们在考试时遇到这种题尽量先跳过(除了后面的题目更毒瘤),有的时候dp不一定是正解所以我们要懂得放弃,不要妄想卡常,优化可以卡过,该打正解还是要打,正如LC所说dp其实就是比普通暴力更优的暴力.