跳到主要內容

[Training] GCJ 2015 Round 2 Kiddie Pool

Kiddie Pool 問題簡化如下:

n 個水龍頭 1...n,每個水龍頭出水速度 Ri,各自水溫為 Ci 。今要混合成溫度 X 且體積 V 的水,求最小時間?

  1. 題目給的溫度公式,跟質心公式一樣。所以目標溫度 X,可以平移到數線 0 的位置。也就是每個水龍頭溫度都改為 X-Ci,目標改為配出溫度 0 度的水,方便計算。
  2. 考慮某個水龍頭開的時間最久,開啟時間為 t 秒,則得知其他的水龍頭最多也只能開啟 t 秒,不能再多。現在的目標為 t 需最小。
  3. 因此本題化簡為:假設最終答案就是 t 秒,每個水龍頭假設都先開啟 t 秒,各自裝入各自的水杯,然後計算有哪些水杯內的水是多餘的,需要倒掉。倒掉後的每杯水體積總和要逼近目標的體積 V,總和超過或小於 V,就要調整 t 的大小,直到體積總和等於 V 為止。
  4. 解上述第 3 點的問題,可以想像成有一蹺蹺板,上面放著 n 杯水。每杯水重量為 Ri × t。每杯水跟支點距離 abs(X-Ci)。為了維持平衡,必須將多餘的水從某幾杯中倒出,微調到蹺蹺板平衡時,所有水杯總重量總和需逼近 V。
  5. 利用貪婪演算法,從靠近支點處往遠離支點處走訪每個水杯,並累計力矩的值,左右兩邊一定會至少有一邊走訪到盡頭,此時若有一邊的力矩比較多,將力矩多的那邊當前走訪到的水杯,透過倒掉水的方式使左右平衡。
    調整到支點左右力矩相等時,檢查水的體積總和是否符合要求,如果總體積太小,就拉高 t 值,否則降低 t 值。t 值的逼近,用 Binary Search 逼近法。
  6. 本題要十分注意的地方是 IMPOSSIBLE 的判斷。如果蹺蹺板支點沒有任何物品,且支點左邊全空或右邊全空,就是 IMPOSSIBLE 的情況。如果支點有物品,或者支點左右都有物品,此時 t 已經快要趨近極大,就代表 IMPOSSIBLE。
核心程式碼如下:
其中 left/right 代表支點左右的力矩,v_max 為當前 t 值下,力矩平衡的最大體積,center 是用來判斷支點有沒有物品。binary search 的 iteration 共 1000 次。

// binary search double low = 0, high = 1e20; for(int it = 0; it < 1000; it++) { double ans = (low + high) * 0.5; double left = 0, right = 0, center = 0, v_max = 0, acc = 0; // my way. for(int i=0; i<n; i++) { v_arr[i] = rate_arr[i]*ans; if(t_arr[i] < 0) left -= (v_arr[i]*t_arr[i]); // this is ok. else if(t_arr[i] > 0) right += (v_arr[i]*t_arr[i]); else center += (v_arr[i]); } if((center == 0) && ((left==0) || (right==0))) { low = 1e20; break; // ok } if(left < right) { for(int i=0; i<n; i++) { if(t_arr[i] > 0) { acc += (v_arr[i] * t_arr[i]); if(acc > left) { double tmp = acc - left; v_max += (v_arr[i] - (tmp/t_arr[i])); break; } else { v_max += v_arr[i]; } } // if t_arr[i] > 0 else { v_max += v_arr[i]; } } } else // left >= right { for(int i=(n-1); i>=0; i--) { if(t_arr[i] < 0) { acc -= (v_arr[i] * t_arr[i]); if(acc > right) { double tmp = acc - right; v_max += (v_arr[i] + (tmp/t_arr[i])); break; } else { v_max += v_arr[i]; } } // if t_arr[i] > 0 else { v_max += v_arr[i]; } } } if(v_max > v) high = ans; else if(v_max < v) low = ans; else high = low = ans; }

以下是我自己出給自己的隨堂測驗跟作業:(需於 2020/01/15 前完成)
  • 如果在上述第 5 項中,貪婪演算法從距離支點較遠的項目往支點方向走訪,則是否能成功解題?

    [Answer] 
    無法解題。
    如果從離支點較遠的地方往支點走訪,根據貪婪演算法,在水龍頭開 t 秒後用此法微調至左右平衡,最後水的總體積並不是最大體積。
    假設支點左右的總力矩,分別是 F1 和 F2,且 F1<F2。
    此時 F1 那邊的水一定都沒有被倒掉。
    F2 的水如果只倒掉靠近支點的那幾杯來取得左右力矩平衡,根據貪婪演算法,平衡後得到的總體積是 F1 那邊不倒掉水的情況下,體積的最小值 Vx。
    因此可以想像,我們有機會透過降低 t 時間,讓 F1 降低,同時找到一個相同的總體積 Vx,使左右力矩平衡。
  • 用上述第 5 項的方法微調力矩到平衡時,會不會有 t 較小時,總體積反而比 t 較大時的體積還多的情況?

    [Answer]
    不會,支點左右的水杯在還沒倒水前裝的水最多,正比於 t 值。t 下降時,體積總和必定下降。
  • 程式對於小數點以下精確度的問題,要怎麼確保?

    [Answer]
    採用以下方式輸出答案,9 代表小數點以下的精確位數:
     cout.precision(9);
     cout << fixed << ans << endl;
    
    或者:
     printf("%.9f\n", ans);
    
  • 解 UVA 10498。

留言

這個網誌中的熱門文章

[程式競賽] UVa 572, Oil Deposits,Flood Fill 演算法

原題目簡述如下: 以 m x n 大小的 grid 代表一張地圖,現今要在此地圖內探勘,找出油田。某一區塊如果標示 "@" 代表有油,"*" 代表沒有油。 "@" 相鄰的區域的聯集,可視為一個油田。(所謂相鄰,除了上下左右,斜向的相鄰也算進去) 求任意地圖中,油田的個數。 例如輸入的測資為: *    *   *    *  @ *   @  @  *  @ *   @   *   *  @ @ @  @   * @ @ @   *   *  @ 則油田個數為 2。 想法 採用典型的倒水演算法(Flood Fill),走訪 "@" 出現的區域,從此往下倒水,倒過水的區域標上 id,因此透過 id 的編號,可以得知油田的個數。 實作 先實作倒水演算法的子函式: void floodfill(vector<vector<char> >& map,                vector<vector<int>  >& id_table,                int row, int col, int id) {    if(row < 0 || (row >= map.size()) )   return;    if(col < 0 || (col >= map[0].size())) return;    if(map[row][col] != '@' || id_table[row][col] > 0) return;    id_table[row][col] = id;   ...

[Python] print 同時輸出到 file 和 console

在 Python 撰寫程式時,我們會希望螢幕 stdout 輸出可以同時記錄到 log 檔案裡。 但是螢幕輸出可能含有 ASCII escape codes 的顏色資訊,輸出的 log 檔案會有類似 ^[[01;32m 這種字樣出現。 我採用比較簡單的解法: 先將 print 函式輸出的訊息,同時導向到螢幕,同時儲存在指定的 log.txt 檔案中。 再用 sed 指令,將 log.txt 內的 ASCII escape code 清除。 方法如下: import sys class PrintLog(object): def __init__(self): self.console = sys.stdout self.log_file = open("log.txt", "w") def write(self, msg): self.console.write(msg) self.log_file.write(msg) def flush(self): pass def main(): original_stdout = sys.stdout sys.stdout = PrintLog() print " This is a testing message." sys.stdout = original_stdout if(__name__ == "__main__"): main() 也就是將 sys.stdout 指向自定義的 PrintLog class,讓 PrintLog 來處理輸出文字,用完 PrintLog 後再把 sys.stdout 導向回原本的 stdout。 接著使用 sed 指令刪除 log.txt 的 ASCII escape code: sed -i 's/\x1b[^m]*m//g' ./log.txt 上面的正規,\x 後面用來接一個 16 進位 ASCII 編碼,其中 1b 代表的是 ESC 退出鍵。 到此即可獲得沒有顏色編碼的 log.tx...

[Linux] 安裝 conda 並用 conda 安裝套件

本篇文章介紹 conda 在 Linux 安裝與基本使用方法。 conda 是一個套件包管理器,跟 apt-get 一樣。conda 的宗旨最初是為了管理複雜的 Python 語言包安裝,後來開始支援其它語言包的安裝(例如 R 語言)。 在 Linux 安裝 conda 指令無法採用 apt-get 指令,要安裝 conda 有兩種方式: 安裝 Anaconda。Anaconda 是一個 Python 的發行版,專門用於計算科學,內建很多的預設數據科學的軟體包,因此會安裝 Anaconda 會需要較大的硬碟空間,會安裝約 3 GB 大的檔案到電腦內。 安裝 Miniconda,是最小安裝版本的 Anaconda,內建 conda、Python 和一些基本套件和基本工具。我目前是安裝這個,因為我不想要安裝一些目前還用不到的語言包。 Minicoda 可以在 這個網站 下載。 我選擇 Python 2.7 的 Linux 64-bit 版本下載,安裝過程 不需要使用 sudo 權限 ,否則之後 conda 執行上會有權限問題,conda 在執行的時候是無法使用 sudo conda ... 來執行指令的。 安裝過程如下(作業系統為 Linux 發行版:Elementary OS): 執行 (不需要加 sudo)  bash ./Miniconda2-latest-Linux-x86_64.sh 會出現歡迎畫面: Welcome to Miniconda2 4.7.12 In order to continue the installation process, please review the license agreement. Please, press ENTER to continue >>> 按 Enter 後,閱讀完授權合約,輸入 yes 接受合約條款。 Do you accept the license terms? [yes|no] [no] >>> 預設安裝路徑是家目錄的 minicoda2,按下 Enter 可即刻安裝。 Miniconda2 will now be installed into this...