1. 程式人生 > 其它 >P1220 關路燈小結

P1220 關路燈小結

萬惡之源

前言

本來只是打算記下筆記的,但是發現越寫越多,心想要不就順便改成題解吧,於是有了此文,本文也參考了一些題解,因此本文僅作為其它題解的一些補充,一些做題可能遇到的問題。

正題

1. 這是一道區間動態規劃(最重要的還是要有)

區間dp:區間dp就是在區間上進行動態規劃,求解一段區間上的最優解。主要是通過合併小區間的 最優解進而得出整個大區間上最優解的dp演算法。

怎麼看出來的呢,首先關燈是沒有代價的,那麼假如我們在最左邊,那麼只需要一路往右走就一定是最優解,但是如果在中間的話,那麼可能一會往左一會往右,假如我們已經知道了某個區間的最優解,那麼這個區間向左或向右再拓展一個單位也仍然會是最優的,因此滿足區間dp的特點

每次的選擇也很簡單,在一個區間裡,要麼往右走,要麼往左走,並且我們可以很容易發現關完一個區間要麼在最左邊,要麼在右邊,否則會帶來額外的浪費,所以我們的選擇就可以表達成,在一個區間的最左邊時,要麼繼續往左走,要麼折返向右走,在最右邊時也同理。由此我們可以得到狀態轉移方程為

dp[i][j][0] = min(dp[i+1][j][0]+power(),dp[i+1][j][1]+power());
dp[i][j][1] = min(dp[i][j-1][0]+power(),dp[i][j-1][1]+power());
//其中dp[a][b][0/1]表示關閉區間[a,b]的燈後在左邊或在右邊 
//power()表示未關的燈的耗能 

2.只有狀態轉移方程還不夠

雖然狀態轉移方程有了,但是這題並沒有那麼容易就可以水過去,這裡i和j的列舉方式也是一個麻煩的點。

首先很顯然dp[c][c][0/1]是為0的,那麼我們可以考慮從中間向兩邊拓展,但是這裡也有兩種方式

一種是外層列舉i,內層列舉j,即先列舉起點再拓展終點

一種是外層列舉j,內層列舉i。即先列舉終點再拓展起點

詳見程式碼↓↓↓

 for(int i=c-1;i>0;i--)
        for(int j=c+1;j<=n;j++)
        {
            dp[i][j][0]=min(dp[i+1][j][0]+power(i,i+1,i,j+1),dp[i+1][j][1]+power(i,j,i,j+1));
            dp[i][j][1]=min(dp[i][j-1][0]+power(i,j,i-1,j),dp[i][j-1][1]+power(j-1,j,i-1,j));
        }//c為初始地點
        
for(int j=c;j<=n;j++){
	for(int i=j-1;i>0;i--){
		dp[i][j][0] = min(dp[i+1][j][0]+power(i,i+1,i,j+1),dp[i+1][j][1]+power(i,j,i,j+1));
		dp[i][j][1] = min(dp[i][j-1][0]+power(i,j,i-1,j),dp[i][j-1][1]+power(j-1,j,i-1,j));
		}
	}

他們並不一定都是正解,為此我們可以嘗試模擬一下,第一種是先列舉起點再終點,拓展方式是這樣的,覺得圖奇醜無比難以接受的話可以直接看下面的結論。
高能預警

外層迴圈即圈圈由深藍到綠再到淺藍拓展,內層迴圈即圈圈向右拓展,由狀態轉移方程可以看出在這種列舉方式下,有一些區間並不能順利的從子結構遞推至父結構(例如深藍的圈圈都沒有不能轉移到右子區間),所以這種方法是不行的,(其實我猜想這種列舉方式可以用於刷表法,但是不知道行不行)再看第二種,先列舉終點,再列舉起點。

列舉方式是藍,粉,橙,紫,圈圈向左拓展。顯然,這種列舉方式在求dp[i][j][0/1]時,其左右的子區間的答案都是已經確定下來了的,所以我們可以輕鬆得到最優解。

總之,對於\(dp[i][j][0]\),我們要確保\(dp[i+1][j][0]/[1]\)\(dp[i][j-1][0]/[1]\)已經更新,於是,我們正序列舉j,正序列舉i。

3. power()函式(計算剩餘能耗)

另外一個很麻煩的點時如何計算剩餘燈的能耗,直觀來講就是全部燈的能耗減去區間燈的能耗乘以時間即可,我們很容易想到可以用字首和(sumv[x]表示從1號燈到x號燈的能耗總和)來計算,但是這裡隱含著一個邊界問題,首先先上程式碼

int power(int i,int j,int l,int r)
//l,r是左開右開區間(取不到) 
//表示從第i號燈走到第j號燈時,除l到r之外的燈(未關的燈)的耗能 
{
    return (L[j]-L[i])*(sumv[n]-(sumv[r-1]-sumv[l]));
}
//(L[j]-L[i])是路程,數值上等於時間
//sumv[x]是x的字首和,(sumv[r-1]-sumv[l])是所求區間的能耗和

這裡為什麼是r-1呢,其實改成r也沒問題,不過就需要你在其它地方也做出相應的更改,這裡使用r-1是保證計算時用的區間時開區間,也就是不取它,達到兩邊的統一,有一種對稱美,看起來舒服一點

最重要的是可以避免一些謎之bug。至於怎麼知道它是左開右開的區間可以寫個測試字首和的程式來試一下,分塊除錯自己的程式可以省去很多幹擾。

附件1:字首和測試程式

附件2:AC程式碼