为什么都是浅谈?深入就掉坑啊,掉坑就要填坑啊,填坑就会发现又挖了更多的坑啊,然后恶性循环啊。
这个坑必须要填的,拖了这么久了。
先拿TYVJ 1305来说吧,此题具体的题面没找到,代码简单的对拍了一下,若存在错误,还请指出。
题目的意思是n个数,在区间m内,找到最大子段和。O(nlogn)可以解决,用单调队列优化下成为线性时间。
容易得到状态转移方程:dp[i] = sum[i] - min{sum[k] | i-m <= k <= i}.然后就是维护m长度的区间内最小值的问题,其实吧,也没什么DP的概念嘛,但是作为入门,姑且认为这是动规吧。
首先维护一个单调递增的队列,这样每次可以取出最小值,然后每个元素入队列。此题的特点在于,取最小值的时候,要先把小于i-m的元素都弹出队列,然后取队首之后,队首不用弹出,因为下次可能还用得到。
数据是:6 4 1 -3 5 1 -2 3 模拟一下;
1.1入队列,此时队列的状态1,maxx = 1
2.-2入队列,1出队列,此时队列元素-2,maxx = 1
3.2入队列,此时队列元素-2,2,maxx = 5
4.3入队列,此时队列元素-2,2,3,maxx=6
5.1入队列,此时队列中元素-2,2,3,1,maxx=6
6.4入队列,此时队列元素-2,2,3,1,maxx=7. 注意此时-2不用出队列,前缀和问题。
代码实现:
/*
Look at the star
Look at the shine for U
*/
#include
#define sl(x) scanf("%lld",&x)
using namespace std;
typedef long long ll;
const int N = 1e6+5;
const ll mod = 1e9+7;
const ll INF = 0x3f3f3f3f;
ll s[N],que[N],dp[N],pre[N];
int main()
{
ll n,m,i,j,tail,head,k;
sl(n);sl(m);
for(i = 1;i <= n;i++) sl(s[i]),pre[i] = pre[i-1]+s[i];
head = tail = 0;
dp[1] = pre[1];
que[tail] = 1;
// 维护一个单调增的队列
for(i = 2;i <= n;i++)
{
while(que[head] < i-m-1 && head <= tail) head++;
dp[i] = pre[i] - pre[que[head]];
while(head <= tail && pre[que[tail]] > pre[i]) tail--;
que[++tail] = i;
}
ll maxx = -INF;
for(i = 1;i <= n;i++) maxx = max(maxx,dp[i]),cout<
单调队列优化的DP,当然一定要有某一部分满足单调性,典型问题就是类似于滑动窗口问题,维护一个区间最值,随着所需要更新的元素变化而变化,然后看一个单调性特别的题。
BZOJ 3831. 这个题好像也没有题面了,我还是对拍了一会,若有错误,请继续指教。
题意:n棵树,每个的高度是si,某屌丝要从1到n去找妹子,他每跳一次可以跳到i+1,i+2...i+k,每次跳跃若是此树高度小于下一棵树,则疲劳值+1,为了保存体力(为什么要保存体力呢???),问最小的消耗是多少。
移动滑块问题来了,考虑有状态转移dp[i] = min(dp[j] + (s[i] > s[j]))。观察此方程如何保证在区间k内得到最优解。易得dp[i]跟dp[j]相差在1之内,设最优解dp[j]是队首元素,若是dp[j]比dp[i]小1,则选择最高的dp[j]去跳到dp[i],若dp[j]==dp[i],则以后得到的解都是>=dp[i]的,所以按dp的大小维护队列时,高度不影响结果,但当dp值相等的时候,要按高度从高到低排序,那么得到单调队列,即:dp值按递增排序,dp值相同时,按高度降序排序,此后的解法类似于第一题。
代码实现:
/*
Look at the star
Look at the shine for U
*/
#include
#define sl(x) scanf("%lld",&x)
using namespace std;
typedef long long ll;
const int N = 1e6+5;
const ll mod = 1e9+7;
const ll INF = 0x3f3f3f3f;
ll dp[N],que[N],s[N];
bool judge(ll x,ll y)
{
if(dp[x] == dp[y]) return s[x] > s[y];
return dp[x] < dp[y];
}
int main()
{
ll n,i,j,k,q;
sl(n);
for(i = 1;i <= n;i++) sl(s[i]);
sl(q);
while(q--)
{
sl(k);
ll head = 1,tail = 1;
que[1] = 1;
for(i = 2;i <= n;i++)
{
while(head <= tail && que[head] < i-k) head++;
dp[i] = dp[que[head]]+(s[que[head]] <= s[i]);
while(tail >= head && judge(i,que[tail])) tail--;
que[++tail] = i;
}
printf("%lld\n",dp[n]);
}
}
以上两个只是入门级别,甚至DP的思想很模糊,那就找到正经的DP题看一下,这种题往往都是输入很多,想找个输入简单且DP明显的很难啊,只能随便挑一个了。
POJ 2373 Dividing the Path 想哭,这个题,怎么就感觉思维跟不上了呢。。。。。。
题意:有一条长为l的线段,有n个奶牛在区间内吃草,还有喷射距离在[a,b]之间的喷头喷水,要求不能将水喷出线段外,有奶牛的地方喷水不能覆盖,求最少用多少个喷头。
我觉得这个题难的地方不是单调队列优化,而是DP。首先奶牛存在的地方都进行一下标记,表示此处只能使用一个喷头,然后,想办法去搞定区间喷射问题,我们可以枚举右端点,然后去判断左端点,左端点的取值范围在i-2*b <= x <= i-2*a之间取值,而且取值的话每次必是偶数,画个图就可以看明白,然后真不了解此时区间取值的,可以去其他博客看看,这里只讲单调队列的实现,就不深究DP了。然后可以得到状态转移方程:dp[i] = min(dp[j])+1。
维护dp[j]用n^2的复杂度显然是超时的,所以此时要考虑线性时间解题,可以发现dp[j]具有单调性啊,而且j也有取值区间,那不是恰好可以用单调队列来维护的吗?然后维护队列阶段:首先是入队列,我们以当前点为右端点,那么所需要维护的点应该是 i-2*a,代表着当前端点是喷射的右端点,Emmmm,看不懂的话还是建议画图看看,然后是出队列,出队列就比较简单了,判断是否满足出队列的条件,不然就是dp[i]设为最大值。
总之,时间匆忙,此题弄得我也是比较晕,过几天再回来看看有什么需要更改的地方,留坑。然后此题的关键好像也是如何去构建DP,单调队列比较容易理解。建议想要了解单调队列的一定要看看此题。
代码实现:
/*
Look at the star
Look at the shine for U
*/
#include
#define sl(x) scanf("%lld",&x)
using namespace std;
typedef long long ll;
const int N = 1e6+5;
const ll mod = 1e9+7;
const ll INF = 0x3f3f3f3f;
ll dp[N],que[N],s[N],vis[N];
int main()
{
ll n,m,l,a,b,i,j,k,x,y;
while(~sl(n))
{
sl(l);sl(a);sl(b);
for(i = 1;i <= n;i++)
{
sl(x);sl(y);vis[x+1]++;vis[y]--;
}
for(i = 1;i <= l;i++) vis[i] += vis[i-1];
ll head = 1,tail = 0;
for(i = 2;i <= l;i += 2)
{
while(head <= tail && que[head] < i-2*b) head++;
if(i-2*a >= 0)
{
while(head <= tail && dp[i-2*a] <= dp[que[tail]]) tail--;
que[++tail] = i-2*a;
}
if(vis[i]) dp[i] = INF;
else
{
if(head <= tail && dp[que[head]] != INF) dp[i] = dp[que[head]]+1;
else dp[i] = INF;
}
}
printf("%lld\n",dp[l] == INF ? -1:dp[l]);
}
}
至此,数组模拟单调性结构已经写完了,感觉收获还可以呀,理解单调队列优化DP的前提是先学会DP,哎,道阻且长,且歌且行。