問題:獨立任務最優調度,又稱雙機調度問題:用兩台處理機A和B處理n個作業。設第i個作業交給機器A處理時所需要的時間是a[i],若由機器B來處理,則所需要的時間是b[i]。現在要求每個作業只能由一台機器處理,每台機器都不能同時處理兩個作業。設計一個動態規划算法,使得這兩台機器處理完這n個作業的時間最短(從任何一台機器開工到最後一台機器停工的總的時間)。研究一個執行個體:n=6, a = {2, 5, 7, 10, 5, 2}, b = {3, 8, 4, 11, 3, 4}.
分析:當完成k個作業,設機器A花費了x時間,機器B所花費時間的最小值肯定是x的一個函數,設F[k][x]表示機器B所花費時間的最小值。則F[k][x]=Min{ F[k-1][x]+b[k], F[k-1][x-a[k]] };其中F[k-1][x]+b[k]表示第k個作業由機器B來處理(完成k-1個作業時機器A花費的時間仍是x),F[k-1][x-a[k]]表示第k個作業由機器A處理(完成k-1個作業時機器A花費的時間是x-a[k])。
那麼單個點對較大值Max(x, F[k][x]),表示此時(即機器A花費x時間的情況下)所需要的總時間。而機器A花費的時間x是變化的,即x=0,1,2……x(max),(理論上x的取值是離散的,但為編程方便,設為整數連續的)由此構成了點對較大值序列。要求整體時間最短,取這些點對較大值序列中最小的即是。
理解痛點在於B是A的函數運算式,也即動態規劃所在。花點時間,看懂運算式,加上思考,理解了這點一切OK,後面的編程實現完全依據這個思想。先用前兩個任務的枚舉樣本來協助理解。
樣本:前兩個作業樣本足矣。
初始化第一個作業:下標以0開始。
首先,機器A所花費時間的所有可能值範圍:0 <= x <= a[0].
設x<0時,設F[0][x]= ∞,則max(x, ∞)= ∞;記法意義見下。
x=0時,F[0][0]=3,則Max(0,3)=3,機器A花費0時間,機器B花費3時間,而此時兩個機器所需時間為3;
x=1時,F[0][1]=3,Max(1,3)=3;
x=2時,F[0][2]=0,則Max(2,0)=2;
那麼上面的點對序列中,可以看出當x=2時,完成第一個作業兩台機器花費最少的時間為2,此時機器A花費2時間,機器B花費0時間。
來看第二個作業:
首先,x的取值範圍是:0 <= x <= (a[0] + a[1]).
當x<0時,記F[1][x] = ∞;這個記法編程使用,因為數組下標不能小於0。在這裡的實際含義是:x是代表完成前兩個作業機器A的時間,a[1]是機器A完成第2個作業的時間,若x<a[1],則勢必第2個作業由機器B來處理,即在Min()中取前者。
x=0,則F[1][0]= Min{ F[0][0]+b[2], F[0][0-a[1]] }= Min{3+8,∞}=11,進而Max(0,11)=11;
x=1,則F[1][1]= Min{ F[0][1]+b[2], F[0][1-a[1]] }= Min{3+8,∞}=11,進而Max(11)=11;
x=2,則F[1][2]= Min{ F[0][2]+b[2], F[0][2-a[1]] }= Min{0+8,∞}=8,進而Max(2,8)=8;
x=3,則F[1][3]= Min{ F[0][3]+b[2], F[0][3-a[1]] }= Min{0+8,∞}=8,進而Max(3,8)=8;
x=4,則F[1][4]= Min{ F[0][4]+b[2], F[0][4-a[1]] }= Min{0+8,∞}=8,進而Max(4,8)=8;
x=5,則F[1][5]= Min{ F[0][5]+b[2], F[0][5-a[1]] }= Min{0+8,3}=3,進而Max(5,3)=5;
x=6,則F[1][6]= Min{ F[0][6]+b[2], F[0][6-a[1]] }= Min{0+8,3}=3,進而Max(6,3)=6;
x=7,則F[1][7]= Min{ F[0][7]+b[2], F[0][7-a[1]] }= Min{0+8,0}=0,進而Max(7,0)=7;
那麼上面的點對序列中,可以看出當x=5時,完成兩個作業兩台機器花費最少的時間為5,此時機器A花費5時間,機器B花費3時間。
演算法時間複雜度:按照上述思想,編程實現,結果如,演算法時間複雜度為O(n*Sum),其中Sum為a中所有元素之和與b中所有元素之和的最小值。王曉東《演算法設計與實驗題解》中提供的方法時間複雜度是O(m*m*n*n*n),其中m為a、b所有元素的最大值。
比較:Sum必然遠遠小於n*m;所以n*Sum<n*n*m,相比之下差了O(n*m)都不止,看來這個思想還是蠻優秀的,竊喜……
這個是去年演算法課上的一道作業,當時絞盡腦汁想了好幾天,中間不乏出現過投機的方法,因為這道題的資料設計比較巧合,貌似也得到了正確結果,但隨便時間序列換位一下即可發現錯誤。最搞笑的是助教在課堂上給出的答案正是投機取巧的錯誤答案。錯誤的解答如下,錯誤的原因對比下自己去想想吧。
T=max(T,min(tai-1+ai,tbi-1+bi))其中tai表示做完第i個任務a的等待時間,同理tbi。
Initial: T=tai=tbi=0;
i=1 ,T=max(0,min(0+2,0+3))=2;
i=2 ,T=max(2,min(2+5,0+8))=7;
i=3 ,T=max(7,min(7+7,0+4))=7;
i=4 ,T=max(7,min(7+10,4+11))=15;
i=5 ,T=max(15,min(7+5,15+3))=15;
i=6 ,T=max(15,min(12+2,15+4))=15。
以下是我用C#編寫的程式碼,儲存了完成前K項任務的所有最優值,並記錄了完成機器的順序。編程思想完全基於前述分析,程式碼中並附有說明。用C++的同學把二維數組的寫法還有輸出等變換一下即可。為說明問題,並沒有進行過多代碼上的最佳化,還有改進之處。附送一張圖,輸出了一些數組的值(美觀起見,沒有輸出完),方便理解。
using System;
namespace zydd
{
class Program
{
//獨立任務最優調度函數,參數為兩組任務和任務個數,最優時間和順序結果。
static void DlrwZydd(int[] a, int[] b, int n, int[] least, string[] result)
{
//首先給一個大值。
for (int i = 0; i < n; i++)
{
least[i] = 99;
}
//若任務只有一台機器完成,求得兩個時間。
int aSum = 0, bSum = 0;
for (int i = 0; i < n; i++)
{
aSum += a[i];
bSum += b[i];
}
//小值加1作為數組的列數,減少儲存空間。
int Sum = 1 + Math.Min(aSum, bSum);
//建立四個行數列數相同的數組,timeA儲存機器A可能用的時間,timeB儲存對應機器B用的時間,
//timeMax記錄兩者共需的時間,即較大的那個;who則標識完成該任務的機器是A還是B。
int[,] timeA = new int[n, Sum];
int[,] timeB = new int[n, Sum];
int[,] timeMax = new int[n, Sum];
char[,] who = new char[n, Sum];
char[] tempRlt = new char[n];//tempRlt記錄機器完成任務的機器順序,並逐一賦值給result
//先計算第1個任務相關值,記錄在四個數組的第0行。
for (int i = 0; i <= a[0]; i++)
{
timeA[0, i] = i;
if (i < a[0])
{
timeB[0, i] = b[0];
who[0, i] = 'b';
}
else
{
timeB[0, i] = 0;
who[0, i] = 'a';
}
timeMax[0, i] = Math.Max(timeA[0, i], timeB[0, i]);
}
//儘管像下面一樣,直接比較即可得出完成第1項任務的最優時間,但由於使用動態規劃,計算上述值是必需的。
if (a[0] <= b[0])
{
least[0] = a[0];
tempRlt[0] = 'a';
}
else
{
least[0] = b[0];
tempRlt[0] = 'b';
}
result[0] = new String(tempRlt);
//計算第2個至第n個任務,分別記錄在四個數組相應行。
for (int k = 1; k < n; k++)
{
//tempSum記錄完成前k項任務機器A最多需要的時間,即全部由A完成需要的時間,亦即機器A所有可能的取值範圍。
int tempSum = 0;
for (int temp = 0; temp <= k; temp++)
{
tempSum += a[temp];
}
//計算出所有可能的點對(timeA,timeB),並取值timeMax。
for (int i = 0; i <= tempSum; i++)
{
//機器A在完成前k項任務時所花費的時間為i。
timeA[k, i] = i;
//i即timeA[k, i],若機器A完成前k項任務的時間 小於 它完成第k項的時間,可能嗎?不可能,所以第k項任務肯定由機器B做。
if (i < a[k])
{
timeB[k, i] = timeB[k - 1, i] + b[k];
who[k, i] = 'b';
}
//按照前述動態規劃方式的思想,確定機器A在花費i時間時,機器B花費的最優時間。
else
{
if ((timeB[k - 1, i] + b[k]) <= timeB[k - 1, i - a[k]])
{
timeB[k, i] = timeB[k - 1, i] + b[k];
who[k, i] = 'b';
}
else
{
timeB[k, i] = timeB[k - 1, i - a[k]];
who[k, i] = 'a';
}
}
//兩台機器花費時間較大的那個為總花費時間。
timeMax[k, i] = Math.Max(timeA[k, i], timeB[k, i]);
}
//處理數組tempSum後面的值。機器A時間全部設為最大,此時機器B則無需花費時間。
for (int i = tempSum + 1; i < aSum; i++)
{
timeA[k, i] = tempSum;
timeB[k, i] = 0;
}
//完成第k項任務後,在timeMax所有可能值中,選取最小值即最優值。
int flag = 0;//記錄最優值所在的位置i值,同時也是機器A所花費的時間。
for (int i = 0; i <= tempSum; i++)
{
if (timeMax[k, i] > 0 && timeMax[k, i] < least[k])
{
least[k] = timeMax[k, i];
flag = i;
}
}
//用來回溯所用機器的順序,需要注意的是機器A所花費的時間和數組的下標一樣,因此可用做標記。
//最後一項任務的回溯路徑已在中標出,便於理解。
tempRlt[k] = who[k, flag];
for (int i = k; i > 0 && flag > 0; i--)
{
//如果是機器A完成第i項任務,則機器A花費的時間減去a[i]就是完成前i-1項任務的時間。
//同時在who中可以確定前一項任務的機器,因為機器A花費的時間可用做位置標記。
if (tempRlt[i] == 'a')
{
flag -= a[i];
tempRlt[i - 1] = who[i - 1, flag];
}
if (tempRlt[i] == 'b')
{
tempRlt[i - 1] = who[i - 1, flag];
}
}
result[k] = new String(tempRlt);
}
}
static void Main(string[] args)
{
const int N = 6;
int[] a = new int[N] { 2, 5, 7, 10, 5, 2 };
int[] b = new int[N] { 3, 8, 4, 11, 3, 4 };
int[] least = new int[N]; //記錄完成任務的最優時間
string[] result = new string[N]; //記錄完成任務機器的順序
DlrwZydd(a, b, N, least, result);
Console.WriteLine();
for (int i = 0; i < N; i++)
{
Console.WriteLine(" 按要求完成前{0}項任務的機器順序為:" + result[i] + " 時間為:{1}" ,i+1,least[i]);
}
}
}
}
轉自http://hi.baidu.com/liongg/item/03938f0fcd75f58a03ce1b6d