Dividing the Path POJ - 2373 詳細題解 (線性DP + 單調佇列優化)
這道題目是一道較為複雜的線性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;
}