P1223 排隊接水
題目描述
有 \(n\) 個人在一個水龍頭前排隊接水,假如每個人接水的時間為 \(T_i\),請程式設計找出這 \(n\) 個人排隊的一種順序,使得 \(n\) 個人的平均等待時間最小。
輸入格式
第一行為一個整數 \(n\)。
第二行 \(n\) 個整數,第 \(i\) 個整數 \(T_i\) 表示第 \(i\) 個人的接水時間 \(T_i\)。
輸出格式
輸出檔案有兩行,第一行為一種平均時間最短的排隊順序;第二行為這種排列方案下的平均等待時間(輸出結果精確到小數點後兩位)。
樣例 #1
樣例輸入 #1
10
56 12 1 99 1000 234 33 55 99 812
樣例輸出 #1
3 2 7 8 1 4 9 6 10 5
291.90
提示
\(1\le n \leq 1000\),\(1\le t_i \leq 10^6\),不保證 \(t_i\) 不重複。
思路:
- 我們可以採取貪心的策略,將接水時間慢的人放在後面排隊,那麼後面的人的排隊時間就較短,這是我們的直覺告訴我們的結果,並且,這也是貪心策略的區域性最優解,我們只需要儘可能的將接水時間長的人排在後面接水,那麼其他人等待的時間就會減少,到最終時,總的接水時間就會最少。
- 那麼我們這種的接水策略是否能嚴格的數學證明呢?其實大部分的時候,我們貪心策略的思想,就是非常正常的常識,只要我們舉不出明顯的反例來證明我們的策略不可行,我們就可以使用貪心策略,不過這題我們還真的可以來進行嚴格的數學證明我們這個策略的可行性。
證明策略的可行性
- 從這裡也可以看出,貪心策略往往與排序是一起出現的,每次列舉最優的解法,最終達到最優的結果!
程式碼:
#include<algorithm>
#include<iostream>
using namespace std;
struct people {
int t;
int id;
}a[1005];
//按接水時間來升序排列
bool compare(people& a, people& b) {
return a.t < b.t;
}
int n;
int main()
{
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> a[i].t;
a[i].id = i;
}
sort(a + 1, a + 1 + n, compare);
for (int i = 1; i <= n; i++) {
cout << a[i].id << " ";
}
cout << endl;
double sum = 0;
//這裡為什麼要✖(n-i)呢,因為第一個人洗澡的時候,後面n-1個人都要等它,第二個人,後面的n-2個人又要等他
for (int i = 1; i <= n - 1; i++) sum += a[i].t*(n-i);
double average = sum / n;
printf("%.2lf", average);
return 0;
}
部分程式碼的解釋:
計算sum
的部分涉及到如下的程式碼段:
double sum = 0;
for (int i = 1; i <= n - 1; i++)
sum += a[i].t * (n - i);
這段程式碼的目的是計算加權平均數的值,其具體含義是在對人員按照t
屬性排序後,計算每個人在佇列中的等待時間乘以其後麵人數的總和。讓我們分析一下為什麼要乘以(n - i)
:
-
排序的影響:
- 陣列
a[]
中的人員按照t
屬性從小到大排序。 - 排序後,
a[i].t
表示第i
個人的處理時間。
- 陣列
-
等待時間的計算:
- 在排序後的順序中,如果第
i
個人在佇列中,那麼他前面有i - 1
個人,因此他的等待時間就是前面所有人的處理時間之和。
- 在排序後的順序中,如果第
-
加權求和的原理:
- 對於第
i
個人,他的等待時間為a[i].t * (n - i)
。這裡(n - i)
表示的是他後面的人數,因為他後面的每個人都要等待他的處理時間。
- 對於第
-
計算總和:
sum += a[i].t * (n - i)
就是把每個人的等待時間乘以後面的人數加起來,得到總的加權等待時間之和。
-
最終的平均值:
average = sum / n
就是將這個加權等待時間之和除以總人數n
,得到的是每個人的平均等待時間。
因此,乘以 (n - i)
的操作是為了正確地計算每個人的等待時間對整體加權平均數的貢獻,確保了按照題目要求正確計算平均值。