题意: 在长为L(<=1000000)的草地(可看成线段)上装喷水头,喷射是以这个喷水头为中心,喷水头的喷洒半径是可调节的,调节范围为[a,b]。要求草地的每个点被且只被一个喷水头覆盖,并且有些连续区间必须被某一个喷水头覆盖而不能由多个喷头分段完全覆盖,求喷水头的最小数
题解: 这道题目是一道较为复杂的线性Dp题目, 我会按照思路详细列举一些本题解题过程
按照动态规划问题的一般解题思路来, 首先, 我们需要定义一个状态dp[i] 表示恰好到覆盖到 [0, i]时最少需要的喷水装置数量, 答案自然也就是dp[L], 把长度作为容量, 是线性Dp的一种常见方案
然后对于dp[i], 我们需要根据题意可以抽取到以下几点信息.
1. i一定是偶数, 因为我们的目的是放置合适的喷水装置来覆盖, 2*r(直径)一定是偶数
2.i一定不在奶牛的范围之内, 因为如果i在某奶牛范围之内, 该奶牛范围也就被分割为了两块, 不符合题中要求的恰好位于一 个喷水器的覆盖范围内
3.i一定大于2*a, 因为最小半径是a
4.对于i在[2*a, 2*b]这个区间时, 我们需要考虑初始化, 把符合 1 2 条件的dp[i]初始化为1
这样的话我们不妨直接把所有元素都初始化为正无穷
这应该就是全部题中可以提取出的条件信息了, 下面我们就要去找到一个合适的状态转移方程
5.对于i > 2*a时, dp[i]在满足12的情况下, 应该在[i-2*b, i-2*a]这个区间中找到一个最小的+1. 这个地方有一点需要仔细理解 一下, 就是dp[i]并不表示把喷水器放在了i, 而是放在了[i-2*b, i-2*a]这个区间中, 依次递推.
可得状态转移方程: dp[i] = min{dp[j] | i-2*b <= j <= i-2*a} +1
代码如下:
#include <cstdio>
#include <iostream>
#include <algorithm>
#include <cstring>
#include <string>
#include <vector>
#include <queue>
#include <cmath>
using namespace std;
#define ms(x, n) memset(x,n,sizeof(x));
typedef long long LL;
const LL maxn = 1010, maxl = 1e6+5;
int n, l, a, b;
struct cow{
int s, e; //一个奶牛
}cows[maxn];
int dp[maxl]; //恰好覆盖到0,i时的最少装置数量
bool vis[maxl]; //该点是否在奶牛范围内
int main()
{
cin >> n >> l >> a >> b;
for(int i = 1; i <= n; i++){
cin >> cows[i].s >> cows[i].e;
for(int j = cows[i].s+1; j < cows[i].e; j++)
vis[j] = 1; //注意是开区间
}
for(int i = 0; i <= l; i++)
dp[i] = 1<<30; //初始化为正无穷
for(int i = 2*a; i < 2*b; i+=2)
if(!vis[i]) dp[i] = 1; //不在奶牛活动范围内初始化为1
for(int i = 2*b; i <= l; i+=2){
if(!vis[i])
for(int j = i-2*b; j <= i-2*a; j++)
dp[i] = min(dp[i], dp[j]+1); //状态转移方程
}
if(dp[l] != 1<<30)
cout << dp[l] << endl;
else
cout << -1 << endl;
return 0;
}
但是问题来了, 如果你老老实实把上面这个程序写了去交的话, 你可能发现, 他的时间复杂度太大了
时间复杂度大致为(L*(2*b-2*a)), 极端情况可能达到1000000 * 1000, 达到十亿量级, 而1s的最大量级一般不超过一亿, 所有要考虑优化
在这里使用的是单调队列优化, 因为对于1*L这个循环它是背包的容量, 是没有办法去优化的, 只能考虑如果优化(2*b-2*a)这个复杂度, 也就是考虑如何才能更快的从[i-2*b, i-2*a]找到最小的dp[j], 毫无意义要使用一个单调队列, 因为单调队列可以定义一个优先级排序的方法, 让优先级最大的元素永远在队首(常数复杂度)
考虑的问题同上大致相同, 注意对边界的一些判定, 还有就是我们要保证队列中不能出现 > i-2*a的点, 因为那样的话就会一直扰乱状态转移方程, 所以在队列初始化时要加个判断 和求出一个dp[i]后也不能直接入队, 而是把dp[i-2*a+2]入队就可以了
#include <cstdio>
#include <iostream>
#include <algorithm>
#include <cstring>
#include <string>
#include <vector>
#include <queue>
#include <cmath>
using namespace std;
#define ms(x, n) memset(x,n,sizeof(x));
typedef long long LL;
const LL maxn = 1010, maxl = 1e6+5;
const LL inf = 1<<30;
int n, l, a, b;
struct node{
int i, f;
bool operator < (const node &a) const { return f > a.f;}
node(int ii=0, int ff=0) : i(ii), f(ff) { }
};
priority_queue<node> minDp; //dp(i-2*b,i-2*a)
int dp[maxl]; //恰好覆盖到0,i时的最少装置数量
bool vis[maxl]; //该点是否在奶牛范围内
int main()
{
cin >> n >> l >> a >> b;
a <<= 1, b <<= 1; //直接把a, b*2;
int s, e; //直接用s,e提取出f(恰好覆盖到0,i时的最少装置数量)即可
for(int i = 1; i <= n; i++){
cin >> s >> e;
for(int j = s+1; j < e; j++)
vis[j] = 1; //注意是开区间
}
for(int i = 0; i <= l; i++)
dp[i] = inf; //初始化为正无穷
for(int i = a; i <= b; i+=2)
if(!vis[i]){
dp[i] = 1;
if(i <= b+2-a) //下一步要从b+2开始求
minDp.push(node(i,1)); //minDp的初始化
}
for(int i = b+2; i <= l; i+=2){
if(!vis[i]){
node cur;
while(!minDp.empty()){
cur = minDp.top();
if(cur.i < i-b) minDp.pop(); //不符合条件的元素直接移除
else break;
}
if(!minDp.empty())
dp[i] = cur.f+1; //状态转移方程
}
if(dp[i-a+2] != inf)
minDp.push(node(i-a+2, dp[i-a+2])); //为求dp[i+2]做好准备
}
if(dp[l] != 1<<30)
cout << dp[l] << endl;
else
cout << -1 << endl;
return 0;
}