HDU OJ 1074: Doing Homework
HDU OJ 1074: Doing Homework(狀壓DP)
序言:
關於DP的題目已經做了不少了,但是對於一些特殊的DP還是掌握不佳,今天啃了一道關於狀態壓縮的DP題,感覺挺有意思。
Problem Description:
Ignatius has just come back school from the 30th ACM/ICPC. Now he has
a lot of homework to do. Every teacher gives him a deadline of handing
in the homework. If Ignatius hands in the homework after the deadline,
the teacher will reduce his score of the final test, 1 day for 1
point. And as you know, doing homework always takes a long time. So
Ignatius wants you to help him to arrange the order of doing homework
to minimize the reduced score.
Input:
The input contains several test cases. The first line of the input is
a single integer T which is the number of test cases. T test cases
follow. Each test case start with a positive integer N(1<=N<=15) which
indicate the number of homework. Then N lines follow. Each line
contains a string S(the subject’s name, each string will at most has
100 characters) and two integers D(the deadline of the subject), C(how
many days will it take Ignatius to finish this subject’s homework).Note: All the subject names are given in the alphabet increasing
order. So you may process the problem much easier.
Output:
For each test case, you should output the smallest total reduced
score, then give out the order of the subjects, one subject in a line.
If there are more than one orders, you should output the alphabet
smallest one.
Sample Input:
2
3
Computer 3 3
English 20 1
Math 3 2
3
Computer 3 3
English 6 3
Math 6 3
Sample Output:
2
Computer
Math
English
3
Computer
English
MathHint:
In the second test case, both Computer->English->Math and Computer-Math->English leads to reduce 3 points, but the word “English” appears earlier than the word “Math”, so we choose the first order. That is so-called alphabet order.
題目大意:
就是說有個選手,被分配了幾個課程任務。完成每個 Course 都有其固定的時間;每個 Course 還有其截止期限。問如何分配做 Course 的順序才能使延期最小化?要求輸出最小的延期,以及字典序下的做 Course 的順序。
解題思路:
題目給出 Course 數 1<=N<=15,顯然用全排列做是必然超時了。我們馬上會想到用DP做,但是如何來表示每個狀態是個大問題。“狀態壓縮”DP就是用來解決這類問題的:
我們假定“0”代表暫時未做,“1”表示已做。那麼,對於 Course 數為 N 的問題,狀態
表示暫時只做了Course 1,再如狀態
表示暫時只做了Course 1 和 Course 3。特別地
表示全部 Course 都已經做完。
上述是一種二進位制的記錄方式,而每個二進位制數唯一映射了一個十進位制數。因此 [0, (1>>N) - 1] 這個範圍內的每一個數便唯一映射了每個狀態。不過,每個狀態僅僅指明瞭當前已經做完的 Course 以及暫未做完的 Course,卻未指明各個 Course 的完成順序。因此,在記錄每個狀態時,我們還要儲存其前驅狀態,則當前狀態和前驅狀態的“差”便是當前狀態新完成的Course。
另外,我們還可以用 1 >> (j - 1)來表示 Course j。例如,對於課程 2 而言:1 >> (j - 1) = 1 >> (2 - 1) = 2 = 0 … 1 0。可以看到,這個二進位制數除了右數第 j 位是1之外,其他皆為0。我們試著將之與狀態 i 進行“按位與(&)”,若結果為0,便可以判斷狀態 i 還未做過Course j,反之則判斷已做。
如果狀態 i 已經做過 Course j,我們可以假定 Course j 正是其剛剛做的Course(或者說最後做的)。那麼,該狀態的前驅便是 i - (1 >> (j - 1)),也就是做 Course j 前的狀態。
現在思路已漸近清晰,我們先用虛擬碼試著完全打通思路。
DP 虛擬碼:
DP()
/* data -> Course表 */
//name -> Course名
//cost -> Course耗時
//dead -> Course限期
/* dp -> 狀態表 */
//totcost -> 某狀態的總耗時
//delay -> 某狀態的總延期
//prev -> 某狀態的前驅
//cour -> 某狀態的新選的課
//dp[0] 起始狀態
dp[0].totcost = 0 dp[0].delay = 0
for i = 1 to (1>>N) -1 //從底至頂,遍歷所有狀態
dp[i].delay = +inf //至初始延期為無窮
for j = N downto 1 //遍歷所有課程
curr = 1>>(j-1) //二進位制表示
if i & curr //如果狀態 i 做過課程 j
prev = i - curr //定位前驅
delay = dp[prev].delay //暫延時
if dp[prev].totcost + data[j].cost > data[j].dead
delay += dp[prev].totcost + data[j].cost -data[j].dead
if delay < dp[i].delay //若更優
//狀態更新
dp[i].delay = delay
dp[i].prev = prev
dp[i].totcost = dp[prev].totcost + data[j].cost
dp[i].cour = j
return dp
注意到,對 j 內迴圈是倒序遍歷的。正序和倒序並不改變最終的總延期,但在某兩個 Course 屬性完全一致時出現選擇順序的不同。題目給定的輸入是字典序的,它要求輸出也按字典序。
暫且不管正序遍歷還是倒序遍歷,先來考慮該思路下記錄的 Course 順序:首先,dp[end].cour 是最後選擇的 Course ,其應該最後輸出。而 dp[dp[end].prev].cour 是倒數第二個選擇的 Course,其應該倒數第二輸出。顯然,可以用一個遞迴函式進行輸出。
Output 虛擬碼:
OUTPUT(k)
while k!=0
OUTPUT(dp[k].prev)
print data[dp[k].cour].name
最後,證明倒序遍歷才是正確的。考慮輸入
3
Computer 3 3
English 6 3
Math 6 3
這裡 N = 3, i 的遍歷區間是[1, 7],第一個 Course 選 Computer 沒有爭議,問題是第二門課先選 English 還是 Math。
當遍歷到 7 = 111時,若內迴圈是倒序遍歷的,我們會先檢索到 3 ,即
Math,將之視為是剛剛完成的。之後會檢索到2,即 English,但這種情況的delay並不優於 Math 的情況(準確說是相等,因此不優於),故不作資料更新。而若內迴圈是正序遍歷的,則會導致輸出不為字典序了。
至此,此題的思路已完全打通,最後附上C++程式碼。
C++ 程式碼:
#include<iostream>
#include<string>
#include<time.h>
#define Max 20
//#define test
using namespace std;
namespace
{
struct course
{
string name;
int dead;
int cost;
}data[Max];
struct pos
{
int prev;//前一個狀態
int totcost;//當前消耗
int delay;//當前延期
int cour;//當前選的課
}dp[1<<Max];
void output(int k)//模擬棧
{
if(!k) return;
output(dp[k].prev);
cout<<data[dp[k].cour].name<<endl;
}
}
int main()
{
#ifdef test
freopen("/Users/oldman/Documents/Xcode/OJ/input.txt", "r", stdin);
freopen("/Users/oldman/Documents/Xcode/OJ/output.txt", "w", stdout);
#endif
int T;
cin>>T;
while(T--)
{
int N;
while(cin>>N)
{
memset(data, 0, sizeof(data));
for(int i = 1; i <= N; ++i)
cin>>data[i].name>>data[i].dead>>data[i].cost;
/* 設定邊界 */
dp[0].prev = -1;
dp[0].totcost = 0;
dp[0].delay = 0;
for(int i = 1; i < (1<<N); ++i)//遍歷每種狀態
{
dp[i].delay = 999999;//初始置無窮
for(int j = N; j >= 1; j--)//下個選j,注意反向!!
{
int curr = 1 << (j - 1);
if(curr & i)//按位與 -> 如果說狀態 i 做過 j 這個作業
{
int prev = i - curr;//我們假定 j 是當前最後一個作業
// i - curr 自然便是 i 狀態前驅
int delay = dp[prev].delay;//前驅的延期
if(dp[prev].totcost + data[j].cost > data[j].dead)
{
delay += (dp[prev].totcost + data[j].cost - data[j].dead);
}
if(delay < dp[i].delay)
{
dp[i].prev = prev;
dp[i].delay = delay;
dp[i].totcost = dp[prev].totcost + data[j].cost;
dp[i].cour = j;
}
}
}
}
cout<<dp[(1<<N) - 1].delay<<endl;
output((1<<N) - 1);
}
}
return 0;
}