Description
烽火台又称烽燧,是重要的军事防御设施,一般建在险要或交通要道上。一旦有敌情发生,白天燃烧柴草,通过浓烟表达信息;夜晚燃烧干柴,以火光传递军情,在某两座城市之间有 n 个烽火台,每个烽火台发出信号都有一定代价。为了使情报准确地传递,在连续 m 个烽火台中至少要有一个发出信号。请计算总共最少花费多少代价,才能使敌军来袭之时,情报能在这两座城市之间准确传递。
Input
第一行:两个整数 N,M。其中N表示烽火台的个数, M 表示在连续 m 个烽火台中至少要有一个发出信号。接下来 N 行,每行一个数 Wi,表示第i个烽火台发出信号所需代价。
Output
一行,表示答案。
Sample Input
5 3
1
2
5
6
2
Sample Output
4
Hint
对于50%的数据,M≤N≤1,000 。 对于100%的数据,M≤N≤100,000,Wi≤100。
分析
这题本质上是一个DP。很容易得出状态转移方程:
F
[
i
]
=
m
i
n
(
F
[
j
]
:
i
−
m
<
j
<
i
)
+
a
[
i
]
F[i]=min(F[j]:i-m<j<i)+a[i]
F[i]=min(F[j]:i−m<j<i)+a[i]
但是直接这样的话时间复杂度就是
O
(
n
∗
n
)
O(n*n)
O(n∗n)
下面讲一下单调队列优化的具体做法。
上图中,状态枚举到i,当m=4时,我们要做的就是在i-3到i-1中找到最小的F[j],那么枚举到i+1时,我们要做的就是要在i-2到i中找到最小的F[j]。上图中我们可以看出,要寻找最小值的区间向后移动了一位,也就是F[i-m+1]的值被抛弃,F[i-1]的值被加入。这里就可以用单调队列处理了,F[i-1]是插队的数据,F[i-1]有资格插队是因为它更优且更靠近i,比它更差的数将被它取代,保留那些数据没有任何好处。而那些已经不再维护区间之外的就不必再对其进行维护,出队即可。
跟模板差不多,只是加上状态转移和统计就完事。
上代码
#include<algorithm>
#include<iostream>
#include<cstring>
#include<cstdio>
#include<cmath>
using namespace std;
int n,m,h,t,a[100001],q[1000001],f[100001];
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++)
{
cin>>a[i];
}
h=0;t=1;
for(int i=1;i<=n;i++)
{
while(h<=t&&f[i-1]<=f[q[t]])//当前的数比队尾的数要更优就切尾,准备插队
{
t--;
}
t++;
q[t]=i-1;//把F[i-1]插入,这里插入下标而不插入值,便于从队头弹出
while(h<=t&&q[h]<i-m)//已经不属于区间维护内的数弹出
{
h++;
}
f[i]=f[q[h]]+a[i];//状态转移方程
}
int ans;
for(int i=n;i>=n-m+1;i--)//统计
{
ans=min(ans,f[i]);//f[i]已经表示了这个区域的最小代价
}
cout<<ans;
return 0;
}
Update on 2022.1.16
另一种写法:
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<queue>
using namespace std;
int n,m,a[100001],f[100001];
deque<int> q;
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++)
{
scanf("%d",&a[i]);
}
q.push_back(0);
for(int i=1;i<=n;i++)
{
while(!q.empty()&&i-q.front()>m) q.pop_front();
f[i]=f[q.front()]+a[i];//状态转移
while(!q.empty()&&f[i]<=f[q.back()]) q.pop_back();
q.push_back(i);
/*这里操作顺序不能变*/
}
int mn=999999999;
while(!q.empty()&&q.front()>n-m)
{
mn=min(mn,f[q.front()]);
q.pop_front();
}
cout<<mn;
return 0;
}