程式設計四連測之第一測[telephonewire]
題目
題目描述
最近,約翰的奶牛們越來越不滿足於牛棚裡一塌糊塗的電話服務,於是,她們要求約翰把那些老舊的電話線換成效能更好的新電話線。新的電話線架設在己有的n根電話線杆上,第i根電話線的高度為hi, ( 1 <= hi<= 100)。電話線總是從一根電話線杆的頂端被弓}到相鄰的那根的頂端,如果這兩根電話線杆的高度hi和hj不同,那麼約翰就必須支付c * |hi - hj|的費用,當然,你不能行動電話線杆,只能按照原有
的順序在相鄰杆間架設電話線。
加高某些電話線杆能減少架設電話線的總費用,儘管這項工作也需要支付一定的費用。更準確的說,如果他把一根電話線杆加高x米的話,他需要付出x^2費用。
請你幫約翰計算一下,如果合理的進行這兩項工作,他最少要在這個電話線改造工程中花多少錢。
輸入
第一行輸入兩個數n (n <= 100000)和c (c <= 100), 含義如上。接下來n行,每行一個整數hi (hi < 100)。
輸出
輸出約翰完成電話線改造工程需要花費的最小費用,保證答案不超過max long int。
樣例輸入
5 2
2
3
5
1
4
樣例輸出
15
解題思路
DP
這個題一看就應該是dp吧,但是是幾維呢?我覺得不太可能會有人認為這是三維吧(就這資料的大小怎麼都會MLE)。
令dp[i][j] 為第i根電線杆高度為j時的最小費用,則可以得到
其中的k是列舉前一個電線杆的高度。
看起來很複雜?沒事,我們來逐一分解。
可以看到, 是跟當前狀態有關,意思也就是說它不參與k的運算,可以獨立在外面。這下就變成了:。
繼續來分,我們現在來把絕對值去掉,就會有兩種情況:①j >= k,就會變成,再把j * c拿出來,就是;
②j <= k,就會是,拿出來,。
這下子,所需要參與運算的,也就是說要藉助以前的狀態的就只剩下k了。
列舉方向
這裡有兩種可能性,一種j >= k,就相當於是在j以前計算,那麼如果說是從小到大計算的話,那麼就可以藉助他的上一個來求解,也就是說dp[i - 1][k]可以藉助dp[i - 1][k - 1]來得到。也就相當於,如果你要求dp[i - 1][k]的話就需要k次迴圈來求了,但是dp[i - 1][k - 1]是已經求了前面k - 1次,只需要藉助它,就只需要O(1)的複雜度。
同理,j <= k的話就應該是從大到小來計算了。
變數充當單調棧
可以從上面看到,如果需要單調棧的話,那麼只需要棧頂的元素就夠了,可是此時棧頂的元素是什麼呢?很顯然就是dp[i - 1][k - 1],可是在用了棧頂之後,就又需要把它彈出去,然後把dp[i][k]裝進去。
形象點說,也就是說每個棧頂只會被用到一次,用完之後就又被踢出去了。
那這樣的話,還不如就用一個變數來充當棧頂來儲存上一輪中的最小值算了。
參考程式碼
/*
這道題可以用dp求解。令dp[i][j] 為第i顆電線杆的高度為j時i前(包含i)的最小值。
可以得到轉移為 dp[i][j] = min(dp[i - 1][k] + |j - k| * c)
k是列舉i - 1顆的高度,但這裡有一個問題,到底是j大還是k大,所以我們需要分兩種情況:
1、j >= k,dp[i][j] = min(dp[i - 1][k] - k * c + j * c),在j ~ 1中間列舉最小值。
本來應該是用單調佇列列舉最小值,但可以看出,此時最小值只跟i - 1的情況有關,所以可以用變數代替。
2、j <= k,dp[i][j] = min(dp[i - 1][k] + k * c - j * c),在j ~ 最高高度列舉。
這裡之所以是列舉到最高高度,是因為沒有必要在超過最高高度了。
同時也用一個變數列舉這之間的最小值
*/
#include <cstdio>
#include <cstring>
using namespace std;
#define MAXN 100000
#define INF 0x3f3f3f3f
#define max(a, b) a > b ? a : b
#define min(a, b) a < b ? a : b
int n, c, h[MAXN + 5], maxh, ans = INF;
int dp[MAXN + 5][105];
void read (int &x){
x = 0;
char c = getchar ();
while (c < '0' || c > '9')
c = getchar ();
while (c >= '0' && c <= '9'){
x = (x << 1) + (x << 3) + c - 48;
c = getchar ();
}
}
void print (int x){
if (x < 0){
putchar ('-');
x = (~x) + 1;
}
if (x / 10)
print (x / 10);
putchar (x % 10 + 48);
}
int main (){
read (n); read (c);
for (int i = 1; i <= n; i++){
read (h[i]);
maxh = max (maxh, h[i]);
}
memset (dp, INF, sizeof (dp));
for (int i = h[1]; i <= maxh; i++){//考慮邊界,dp[1]的情況很單一
dp[1][i] = (i - h[1]) * (i - h[1]);
}
for (int i = 2; i <= n; i++){
int min1 = INF, min2 = INF;
for (int j = 0; j < h[i]; j++){//算出從0到j - 1的最小值,即是上面的1情況
min1 = min (min1, dp[i - 1][j] - j * c);
}
for (int j = h[i]; j <= maxh; j++){
dp[i][j] = min (dp[i][j], min1 + (j - h[i]) * (j - h[i]) + j * c);
min1 = min (min1, dp[i][j]);//因為從下到上考慮,所以dp[i][j]也要“放進佇列”考慮一下
}
for (int j = maxh; j >= h[i]; j--){
min2 = min (min2, dp[i - 1][j] + j * c);
dp[i][j] = min (dp[i][j], min2 + (j - h[i]) * (j - h[i]) - j * c);
}
}
for (int i = 1; i <= maxh; i++){
ans = min (ans, dp[n][i]);
}
print (ans);
}
當然,這裡有心的人也許會看到,整個dp陣列實際上只跟i和i - 1youg有關,那麼完全可以用一個滾動陣列來求解。
不過這裡就先不粘滾動陣列的程式碼了。有興趣的可以看這裡