本系列是這本算法教材的擴展:《算法競賽入門到進階》(京東 噹噹) 清華大學出版社 PDF下載地址:https://github.com/luoyongjun999/code 其中的「補充資料」 若有建議,請聯繫:(1)QQ 羣,567554289;(2)做者QQ,15512356php
[toc]html
二分法和三分法是算法競賽中常見的算法思路,本文介紹了它們的理論背景、模板代碼、典型題目。c++
在《計算方法》教材中,關於非線性方程的求根問題,有一種是二分法。 方程求根是常見的數學問題,知足方程: $f(x) = 0$ (1-1) 的數$x'$稱爲方程(1-1)的根。 所謂非線性方程,是指$f(x)$中含有三角函數、指數函數或其餘超越函數。這種方程,很難或者沒法求得精確解。不過,在實際應用中,只要獲得知足必定精度要求的近似解就能夠了,此時,須要考慮2個問題: (1)根的存在性。用這個定理斷定:設函數在閉區間$[a, b]$上連續,且$f(a) ∙ f(b) < 0$,則$f(x) = 0$存在根。 (2)求根。通常有兩種方法:搜索法、二分法。 搜索法:把區間$[a, b]$分紅$n$等份,每一個子區間長度是∆x,計算點$x_k = a + k∆x$, $(k=0,1,2,3,4,...,n)$的函數值$f(x_k)$,若$f(x_k) = 0$,則是一個實根,若相鄰兩點知足$f(x_k) ∙ f(x_{k+1}) < 0$,則在$(x_k, x_{k+1})$內至少有一個實根,能夠取($x_k+ x_{k+1})/2$爲近似根。 二分法:若是肯定$f(x)$在區間$[a, b]$內連續,且$f(a) ∙ f(b) < 0$,則至少有一個實根。二分法的操做,就是把$[a, b]$逐次分半,檢查每次分半後區間兩端點函數值符號的變化,肯定有根的區間。 什麼狀況下用二分?兩個條件:<font color=#FF0033>上下界$[a, b]$肯定、函數在$[a, b]$內單調</font>。git
<div align=center><img src="https://img2020.cnblogs.com/blog/1954999/202003/1954999-20200304125222269-1547842922.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkxNDU5Mw==,size_16,color_FFFFFF,t_70" width="30%"></div> <center>圖1.1 單調函數</center>github
複雜度:通過n次二分後,區間會縮小到$(b - a)/2^n$。給定$a$、$b$和精度要求$ε$,能夠算出二分次數$n$,即知足$(b - a)/2^n <ε$。因此,二分法的複雜度是$O(logn)$的。例如,若是函數在區間$[0, 100000]$內單調變化,要求根的精度是$10^{-8}$,那麼二分次數是44次。 二分很是高效。因此,若是問題是單調性的,且求解精確解的難度很高,能夠考慮用二分法。 在算法競賽題目中,有兩種題型:整數二分、實數二分。整數域上的二分,注意終止邊界、左右區間的開閉狀況,避免漏掉答案或者死循環。實數域上的二分,須要注意精度問題。算法
先看一個簡單問題:在有序數列a[]中查找某個數x;若是數列中沒有x,找它的後繼。經過這個問題,給出二分法的基本代碼。 若是有x,找第一個x的位置;若是沒有x,找比x大的第一個數的位置。數組
<div align=center><img src="https://img2020.cnblogs.com/blog/1954999/202003/1954999-20200304111020679-183077438.jpg" width="60%"></div> <center> 圖2.1 (a)數組中有x (b)數組中沒有x </center>數據結構
示例:a[] = {-12,-6,-4,3,5,5,8,9},其中有n = 8個數,存儲在a[0]~a[7]。 1)查找x = -5,返回位置2,指向a[2] = -4; 2)查找x = 7,返回位置6,指向a[6] = 8; 3)特別地,若是x 大於最大的a[7] = 9,例如x = 12,返回位置8。因爲不存在a[8],因此此時是越界的。 下面是模板代碼。函數
<center>查找大於等於 x的最小的一個的位置(x或者x的後繼)</center> ```c int bin_search(int *a, int n, int x){ //a[0]~a[n-1]是單調遞增的 int left = 0, right = n; //注意:不是 n-1 while (left < right) { int mid = left + (right-left)/2; //int mid = (left + right) >> 1; if (a[mid] >= x) right = mid; else left = mid + 1; } //終止於left = right return left; //特殊狀況:a[n-1] < x時,返回n } ```學習
下面對上述代碼進行補充說明: (1)代碼執行完畢後,left==right,二者相等,即答案所處的位置。 (2)複雜度:每次把搜索的範圍縮小一半,總次數是log(n)。 (3)中間值mid 中間值寫成mid = left + (right-left)/2 或者mid = (left + right) >> 1都行 [參考李煜東《算法競賽進階指南》26頁,有mid = (left + right) >> 1的細節解釋]。不過,若是left + right很大,可能溢出,用前一種更好。 不能寫成 mid = (left + right)/2; 在有負數的狀況下,會出錯。 (4)對比測試 bin_search()和STL的lower_bound()的功能是同樣的。下面的測試代碼,比較了bin_search()和lower_bound()的輸出,以此證實bin_search()的正確性。注意,當a[n-1]<key時,lower_bound()返回的也是n。 代碼執行如下步驟: 1)生成隨機數組a[]; 2)用sort()排序; 3)生成一個隨機的x; 4)分別用bin_search()和lower_bound()在a[]中找x; 5)比較它們的返回值是否相同。
<center>bin_search()和lower_bound()對比測試</center>
#include<bits/stdc++.h> using namespace std; #define MAX 100 //試試10000000 #define MIN -100 int a[MAX]; //若是MAX超過100萬,大數組a[MAX]最好定義爲全局。 //大數組定義在全局的緣由是:有的評測環境,棧空間很小,大數組定義在局部佔用了棧空間致使爆棧。 //如今各大OJ和比賽都會設置編譯命令使棧空間等於內存大小,不會出現爆棧。 unsigned long ulrand(){ //生成一個大隨機數 return ( (((unsigned long)rand()<<24)& 0xFF000000ul) |(((unsigned long)rand()<<12)& 0x00FFF000ul) |(((unsigned long)rand()) & 0x00000FFFul)); } int bin_search(int *a, int n, int x){ //a[0]~a[n-1]是有序的 int left = 0, right = n; //不是 n-1 while (left < right) { int mid = left+(right-left)/2; //int mid = (left+ right)>>1; if (a[mid] >= x) right = mid; else left = mid + 1; } return left; //特殊狀況:若是最後的a[n-1] < key,left = n } int main(){ int n = MAX; srand(time(0)); while(1){ for(int i=0; i< n; i++) //產生[MIN, MAX]內的隨機數,有正有負 a[i] = ulrand() % (MAX-MIN + 1) + MIN; sort(a, a + n ); //排序,a[0]~a[n-1] int test = ulrand() % (MAX-MIN + 1) + MIN; //產生一個隨機的x int ans = bin_search(a,n,test); int pos = lower_bound(a,a+n,test)-a; //比較bin_search()和lower_bound()的輸出是否一致: if(ans == pos) cout << "!"; //正確 else { cout << "wrong"; break;} //有錯,退出 } }
若是隻是簡單地找x或x附近的數,就用STL的lower_bound()和upper_bound()函數。有如下狀況: (1)查找第一個大於x的元素的位置:upper_bound()。代碼例如: pos = upper_bound(a, a+n, test) - a; (2)查找第一個等於或者大於x的元素:lower_bound()。 (3)查找第一個與x相等的元素:lower_bound()且 = x。 (4)查找最後一個與x相等的元素:upper_bound()的前一個且 = x。 (5)查找最後一個等於或者小於x的元素:upper_bound()的前一個。 (6)查找最後一個小於x的元素:lower_bound()的前一個。 (7)單調序列中數x的個數:upper_bound() - lower_bound()。
尋找指定和的整數對。這是一個很是直接的二分法問題。 ∎問題描述 輸入n ( n≤100,000)個整數,找出其中的兩個數,它們之和等於整數m(假定確定有解)。題中全部整數都能用int 表示。 ∎題解 下面給出三種方法: (1)暴力搜,用兩重循環,枚舉全部的取數方法,複雜度$O(n^2)$。超時。 (2)二分法。首先對數組從小到大排序,複雜度$O(nlogn)$;而後,從頭至尾處理數組中的每一個元素a[i],在a[i]後面的數中二分查找是否存在一個等於 m - a[i]的數,複雜度也是$O(nlogn)$。兩部分相加,總複雜度仍然是$O(nlogn)$。 (3)<font color=#FF0033>尺取法</font>/雙指針/two pointers。對於這個特定問題,更好的、標準的算法是:首先對數組從小到大排序;而後,設置兩個變量L和R,分別指向頭和尾,L初值是0,R初值是n-1,檢查$a[L]+a[R]$,若是大於m,就讓R減1,若是小於m,就讓L加1,直至$a[L]+a[R] = m$。排序複雜度$O(nlogn)$,檢查的複雜度$O(n)$,總複雜度$O(nlogn)$。檢查的代碼這樣寫:
void find_sum(int a[], int n, int m){ sort(a, a + n - 1); //先排序 int L = 0, R = n - 1; //L指向頭,R指向尾 while (L < R){ int sum = a[L] + a[R]; if (sum > m) R--; if (sum < m) L++; if (sum == m){ cout << a[L] << " " << a[R] << endl; //打印一種狀況 L++; //可能有多種狀況,繼續 } } }
上面給出的二分法代碼bin_search(),處理的是簡單的數組查找問題。從這個例子,咱們能學習到二分法的思想。 在用二分法的典型題目中,主要是用二分法思想來進行斷定。它的基本形式是:
while (left < right) { int ans; //記錄答案 int mid = left+(right-left)/2; //二分 if (check(mid)){ //檢查條件,若是成立 ans = mid; //記錄答案 … //移動left(或right) } else … //移動right(或left) }
因此,二分法的難點在於如何建模和check()條件,其中可能會套用其餘算法或者數據結構。 二分法的典型應用有:<font color=#FF0033>最小化最大值、最大化最小值。</font>
這是典型的最大值最小化問題。 ∎題目描述 例如,有一個序列{2,2,3,4,5,1},將其劃分紅3個連續的子序列S(1)、S(2)、S(3),每一個子序列最少有一個元素,要求使得每一個子序列的和的最大值最小。 下面舉例2個分法: 分法1:S(1)、S(2)、S(3)分別是(2,2,3)、(4,5)、(1),子序列和分別是七、九、1,最大值是9; 分法2:(2,2,3)、(4)、(5,1),子序列和是七、四、6,最大值是7。 分法2更好。 ∎題解 在一次劃分中,考慮一個x,使x知足:對任意的S(i),都有S(i)<=x,也就是說,x是全部S(i)中的最大值。題目須要求的就是找到這個最小的x。這就是<font color=#FF0033>最大值最小化</font>。 如何找到這個x?從小到大一個個地試,就能找到那個最小的x。 簡單的辦法是:枚舉每個x,用貪心法每次從左向右儘可能多劃分元素,S(i)不能超過x,劃分的子序列個數不超過m個。這個方法雖然可行,可是枚舉全部的x太浪費時間了。 改進的辦法是:用二分法在[max, sum]中間查找知足條件的x,其中max是序列中最大元素,sum是全部元素的和。
「通往奧格瑞瑪的道路」,來源:https://www.luogu.org/problem/P1462 ∎題目描述 給定無向圖,n個點,m條雙向邊,每一個點有點權fi(這個點的過路費),有邊權ci(這條路的血量)。求起點1到終點N的全部可能路徑中,在總邊權(總血量)不超過給定的b的前提下,所通過的路徑中最大點權(這條路徑上過路費最大的那個點)的最小值是多少。 題目數據:n≤10000,m≤50000,fi,ci,B≤1e9。 ∎題解 對點權fi進行二分,用dijkstra求最短路,檢驗總邊權是否小於b。二分法是最小化最大值問題。 這一題是二分法和最短路算法的簡單結合。 (1)對點權(過路費)二分。題目的要求是:從1到N有不少路徑,其中的一個可行路徑Pi,它有一個點的過路費最大,記爲Fi;在全部可行路徑中,找到那個有最小F的路徑,輸出F。解題方案是:先對全部點的fi排序,而後用二分法,找符合要求的最小的fi。二分次數log(fi)=log(1e9) < 30。 (2)在檢查某個fi時,刪除全部大於fi的點,在剩下的點中,求1到N的最短路,看總邊權是否小於b,若是知足,這個fi是合適的(若是最短路的邊權都大於b,那麼其餘路徑的總邊權就更大,確定不符合要求)。一次Dijkstra求最短路,複雜度是O(mlogn)。 總複雜度知足要求。
∎題目描述 「進擊的奶牛」,來源:https://www.luogu.org/problem/P1824 在一條很長的直線上,指定n個座標點(x1, ..., xn)。有c頭牛,安排每頭牛站在其中一個點(牛棚)上。這些牛喜歡打架,因此儘可能距離遠一些。問最近的兩頭牛之間距離的最大值能夠是多少。 這個題目裏,全部的牛棚兩兩之間的距離有個最小值,題目要求使得這個最小值最大化。 ∎題解 (1)暴力法。從小到大枚舉最小距離的值dis,而後檢查,若是發現有一次不行,那麼上次枚舉的就是最大值。如何檢查呢?用貪心法:第一頭牛放在x1,第二頭牛放在xj≥x1+dis的點xi,第三頭牛放在xk≥xj+dis的點xk,等等,若是在當前最小距離下,不能放c條牛,那麼這個dis就不可取。複雜度O(nc)。 (2)二分。分析從小到大檢查dis的過程,發現能夠用二分的方法找這個dis。這個dis符合二分法:它有上下邊界、它是單調遞增的。複雜度O(nlogn)。
#include<bits/stdc++.h> using namespace std; int n,c,x[100005];//牛棚數量,牛數量,牛棚座標 bool check(int dis){ //當牛之間距離最小爲dis時,檢查牛棚夠不夠 int cnt=1, place=0; //第1頭牛,放在第1個牛棚 for (int i = 1; i < n; ++i) //檢查後面每一個牛棚 if (x[i] - x[place] >= dis){ //若是距離dis的位置有牛棚 cnt++; //又放了一頭牛 place = i; //更新上一頭牛的位置 } if (cnt >= c) return true; //牛棚夠 else return false; //牛棚不夠 } int main(){ scanf("%d%d",&n, &c); for(int i=0;i<n;i++) scanf("%d",&x[i]); sort(x,x+n); //對牛棚的座標排序 int left=0, right=x[n-1]-x[0]; //R=1000000也行,由於是log(n)的,很快 //優化:把二分上限設置爲1e9/c int ans = 0; while(left < right){ int mid = left + (right - left)/2; //二分 if(check(mid)){ //當牛之間距離最小爲mid時,牛棚夠不夠? ans = mid; //牛棚夠,先記錄mid left = mid + 1; //擴大距離 } else right = mid; //牛棚不夠,縮小距離 } cout << ans; //打印答案 return 0; }
實數域上的二分,比整數二分簡單。 <center>實數二分的基本形式</center>
const double eps =1e-7; //精度。若是下面用for,能夠不要eps while(right - left > eps){ //for(int i = 0; i<100; i++){ double mid = left+(right-left)/2; if (check(mid)) right = mid; //斷定,而後繼續二分 else left = mid; }
其中,循環用2種方法均可以:
while(right - left > eps) { ... } 或者: for(int i = 0; i < 100; i++) { ... }
若是用for循環,因爲循環內用了二分,執行100次,至關於實現了 $1/2^100$的精度,通常比eps更精確。 for循環的100次,比while的循環次數要多。若是時間要求不是太苛刻,用for循環更簡便*[參考李煜東《算法競賽進階指南》27頁的說明]*。
∎題目描述 「Pie」,題目來源:http://poj.org/problem?id=3122 主人過生日,m我的來慶生,有n塊半徑不一樣的圓形蛋糕,由m+1我的(加上主人)分,每人的蛋糕必須同樣重,並且是一整塊(不能是幾個蛋糕碎塊,也就是說,每一個人的蛋糕都是從一塊圓蛋糕中切下來的完整一塊)。問每一個人能分到的最大蛋糕是多大。 ∎題解 最小值最大化問題。設每人能分到的蛋糕大小是x,用二分法枚舉x。 <center>「Pie」題的代碼</center>
#include<stdio.h> #include<math.h> double PI = acos(-1.0); //3.141592653589793; #define eps 1e-5 double area[10010]; int n,m; bool check(double mid){ int sum = 0; for(int i=0;i<n;i++) //把每一個圓蛋糕都按大小mid分開。統計總數 sum += (int)(area[i] / mid); if(sum >= m) return true; //最後看總數夠不夠m個 else return false; } int main(){ int T; scanf("%d",&T); while(T--){ scanf("%d%d",&n,&m); m++; double maxx = 0; for(int i=0;i<n;i++){ int r; scanf("%d",&r); area[i] = PI*r*r; if(maxx < area[i]) maxx = area[i]; //最大的一塊蛋糕 } double left = 0, right = maxx; for(int i = 0; i<100; i++){ //while((right-left) > eps) { //for或者while都行 double mid = left+(right-left)/2; if(check(mid)) left = mid; //每人能分到mid大小的蛋糕 else right = mid; //不夠分到mid大小的蛋糕 } printf("%.4f\n",left); // 打印right也對 } return 0; }
飢餓的奶牛 https://www.luogu.org/problem/P1868 尋找段落 https://www.luogu.org/problem/P1419 小車問題 https://www.luogu.org/problem/P1258
借教室 https://www.luogu.org/problem/P1083
跳石頭 https://www.luogu.org/problem/P2678 聰明的質監員 https://www.luogu.org/problem/P1314 分梨子 https://www.luogu.org/problem/P1493
第k大 http://acm.hdu.edu.cn/showproblem.php?pid=6231
三分法求單峯(或者單谷)的極值,是二分法的一個簡單擴展。 單峯函數和單谷函數以下圖,函數f(x)在區間[l, r]內,只有一個極值v,在極值點兩邊,函數是單調變化的。以單峯函數爲例,在v的左邊,函數是嚴格單調遞增的,在v右邊是嚴格單調遞減的。
<div align=center><img src="https://img2020.cnblogs.com/blog/1954999/202003/1954999-20200304125834578-73426390.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkxNDU5Mw==,size_16,color_FFFFFF,t_70" width="70%"></div> <center>圖6.1 (1)單峯函數 (2)單谷函數</center>
下面的講解都以求單峯極值爲例。 如何求單峯函數最大值的近似值?雖然不能直接用二分法,不過,只要稍微變形一下,就能用了。 在[l, r]上任取2個點,mid1和mid2,把函數分紅三段。有如下狀況: (1)若f(mid1) < f(mid2),極值點v必定在mid1的右側。此時,mid1和mid2要麼都在v的左側,要麼分別在v的兩側。以下圖所示。
<div align=center><img src="https://img2020.cnblogs.com/blog/1954999/202003/1954999-20200304125932965-1232705578.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkxNDU5Mw==,size_16,color_FFFFFF,t_70" width="60%"></div> <center>圖6.2 狀況(1):極值點v在mid1右側</center>
下一步,令l = mid1,區間從[l, r]縮小爲[mid1, r],而後再繼續把它分紅三段。 (2)同理,若f(mid1) > f(mid2),極值點v必定在mid2的左側。以下圖所示。下一步,令 r = mid2,區間從[l, r]縮小爲[l, mid2]。
<div align=center><img src="https://img2020.cnblogs.com/blog/1954999/202003/1954999-20200304130022831-1321455925.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkxNDU5Mw==,size_16,color_FFFFFF,t_70" width="60%"></div> <center>圖6.3 狀況(2):極值點v在mid1右側</center>
不斷縮小區間,就能使得區間[l, r]不斷逼近v,從而獲得近似值。 如何取mid1和mid2?有2種基本方法: (1)三等分:mid1和mid2爲[l, r]的三等分點。那麼區間每次能夠減小三分之一。 (2)近似三等分:計算[l, r]中間點mid = (l + r) / 2,然讓mid1和mid2很是接近mid,例如mid1 = mid - eps,mid2 = mid + eps,其中eps是一個很小的值。那麼區間每次能夠減小接近一半。 方法(2)比方法(1)要稍微快一點。 (<font color=#FF0033>有網友說</font>不要用方法(2):「由於在有些狀況下這個 eps 太小可能致使這兩個算出來的相等,若是相等就有可能會判斷錯方向,因此其實不建議這麼寫,log3 和 log2 本質上是同樣的。」) 注意:單峯函數的左右兩邊要嚴格單調,不然,可能在一邊有f(mid1) == f(mid2),致使沒法判斷如何縮小區間。
下面用一個模板題給出實數三分的兩種實現方法。 ∎題目描述 「模板三分法」,來源:https://www.luogu.com.cn/problem/P3382
給出一個N次函數,保證在範圍[l, r]內存在一點x,使得[l, x]上單調增,[x, r]上單調減。試求出x的值。 ∎題解 下面分別用前面提到的2種方法實現:(1)三等分;(2)近似三等分。 (1)三等分
<center>mid1和mid2爲[l, r]的三等分點</center>
#include<bits/stdc++.h> using namespace std; const double eps = 1e-6; int n; double a[15]; double f(double x){ //計算函數值 double s=0; for(int i=n;i>=0;i--) //注意函數求值的寫法 s = s*x + a[i]; return s; } int main(){ double L,R; scanf("%d%lf%lf",&n,&L,&R); for(int i=n;i>=0;i--) scanf("%lf",&a[i]); while(R-L > eps){ // for(int i = 0; i<100; i++){ //用for也行 double k =(R-L)/3.0; double mid1 = L+k, mid2 = R-k; if(f(mid1) > f(mid2)) R = mid2; else L = mid1; } printf("%.5f\n",L); return 0; }
(2)近似三等分
<center>mid1和mid2在[l, r]的中間點附近</center>
#include<bits/stdc++.h> using namespace std; const double eps = 1e-6; int n; double a[15]; double f(double x){ double s=0; for(int i=n;i>=0;i--) s=s*x+a[i]; return s; } int main(){ double L,R; scanf("%d%lf%lf",&n,&L,&R); for(int i=n;i>=0;i--) scanf("%lf",&a[i]); while(R-L > eps){ // for(int i = 0; i<100; i++){ //用for也行 double mid = L+(R-L)/2; if(f(mid - eps) > f(mid)) R = mid; else L = mid; } printf("%.5f\n",L); return 0; }
(1)「三分求極值」,題目來源:http://hihocoder.com/problemset/problem/1142 ∎題目描述:在直角座標系中有一條拋物線y = ax^2 + bx + c和一個點P(x, y),求點P到拋物線的最短距離d。 ∎題解:直接求距離很麻煩。觀察這一題的距離D,發現它知足單谷函數的特徵,用三分法很合適。 (2)<font color=#FF0033>三分套三分</font>,是計算幾何的常見題型。 「Line belt」,題目來源:http://acm.hdu.edu.cn/showproblem.php?pid=3400 ∎題目描述:給定兩條線段AB、CD ,一我的在AB上跑的時候速度是p,在CD上速度是q,在其餘地方跑速度是r。問從A點到D點最少的時間。 ∎題解:從A出發,先走到AB上一點X,而後走到CD上一點Y,最後到D。時間是: time = |AX|/p + |XY|/r + |YD|/q 假設已經肯定了X,那麼目標就是在CD上找一點Y,使|XY|/r + |YD|/q最小,這是個單峯函數。三分套三分就能夠了。
整數三分的形式是:
while (left < right) { int mid1 = left + (right - left)/3; int mid2 = right- (right - left)/3; if(check(mid1) > check(mid2)) … //移動right else … //移動left }
下面是一個例題。 ∎題目描述 「期末考試」,題目來源:https://www.lydsy.com/JudgeOnline/problem.php?id=4868 有n位同窗,每位同窗都參加了所有的m門課程的期末考試,都在焦急的等待成績的公佈。第i位同窗但願在第ti天或以前得知全部課程的成績。若是在第ti天,有至少一門課程的成績沒有公佈,他就會等待最後公佈成績的課程公佈成績,每等待一天就會產生C不愉快度。對於第i門課程,按照本來的計劃,會在第bi天公佈成績。有以下兩種操做能夠調整公佈成績的時間:1.將負責課程X的部分老師調整到課程Y,調整以後公佈課程X成績的時間推遲一天,公佈課程Y成績的時間提早一天;每次操做產生A不愉快度。2.增長一部分老師負責學科Z,這將致使學科Z的出成績時間提早一天;每次操做產生B不愉快度。上面兩種操做中的參數X,Y,Z都可任意指定,每種操做都可以執行屢次,每次執行時均可以從新指定參數。如今但願你經過合理的操做,使得最後總的不愉快度之和最小,輸出最小的不愉快度之和便可。 ∎題解 不愉快度是一個下凹的函數,用三分法。代碼略。