1. 程式人生 > >(2017)第八屆藍橋杯大賽個人賽省賽(軟體類) C/C++ 大學A組 題解(第九題)

(2017)第八屆藍橋杯大賽個人賽省賽(軟體類) C/C++ 大學A組 題解(第九題)

前言

在回北京的火車上,今天就先解決一道簡單的題吧=w=

第九題

題目

標題: 分巧克力
兒童節那天有K位小朋友到小明家做客。小明拿出了珍藏的巧克力招待小朋友們。
小明一共有N塊巧克力,其中第i塊是Hi x Wi的方格組成的長方形。
為了公平起見,小明需要從這 N 塊巧克力中切出K塊巧克力分給小朋友們。切出的巧克力需要滿足:
1. 形狀是正方形,邊長是整數  
2. 大小相同  
例如一塊6x5的巧克力可以切出6塊2x2的巧克力或者2塊3x3的巧克力。
當然小朋友們都希望得到的巧克力儘可能大,你能幫小Hi計算出最大的邊長是多少麼?
輸入
第一行包含兩個整數N和K。(1 <= N, K <= 100000)  
以下N行每行包含兩個整數Hi和Wi。(1 <= Hi, Wi <= 100000) 
輸入保證每位小朋友至少能獲得一塊1x1的巧克力。   
輸出
輸出切出的正方形巧克力最大可能的邊長。
樣例輸入:
2 10  
6 5  
5 6  
樣例輸出:
2
資源約定:
峰值記憶體消耗(含虛擬機器) < 256M
CPU消耗  < 1000ms
請嚴格按要求輸出,不要畫蛇添足地列印類似:“請您輸入...” 的多餘內容。
注意:
main函式需要返回0;
只使用ANSI C/ANSI C++ 標準;
不要呼叫依賴於編譯環境或作業系統的特殊函式。
所有依賴的函式必須明確地在原始檔中 #include <xxx>
不能通過工程設定而省略常用標頭檔案。
提交程式時,注意選擇所期望的語言型別和編譯器型別。

第一種做法

分析

首先題目保證每個小朋友可以分到1×1的巧克力。那麼我們就慢慢擴大分割後每塊巧克力的大小,看看這種大小的巧克力是否滿足條件即可。

這樣我們可以發現一個規律:如果某種大小的巧克力無法滿足條件,那麼所有比它大的巧克力都無法滿足該條件。因為巧克力一定是越大,分出的塊數更少的。

程式碼及執行結果

#include <cstdio>
using namespace std;
const int MAX_N = 1e5 + 5;
long long h[MAX_N], w[MAX_N];
int main() {
    int n, k;
    scanf("%d%d", &n, &k);
    for (int i = 0; i < n; i++) {
        scanf("%lld%lld", &h[i], &w[i]);
    }
    int ans = 1; // 這裡不需要longlong,因為答案不可能比原來的巧克力還要大
    for (int i = 2; ; i++) {
        long long sum = 0; // 這個數可能會溢位int
        for (int j = 0; j < n; j++) {
            sum += (h[j] / i) * (w[j] / i);
        }
        if (sum < k) break;
        else ans++;
    }
    printf("%d\n", ans);
    return 0;
}

執行結果:


第二種做法

分析

我們成功的完成了這道題並通過了樣例。但是有一個問題,這個程式能否通過所有的樣例呢?從正確性來講是沒有問題的,但是我們再次觀察一下資料範圍。

輸入
第一行包含兩個整數N和K。(1 <= N, K <= 100000)  
以下N行每行包含兩個整數Hi和Wi。(1 <= Hi, Wi <= 100000) 

輸入保證每位小朋友至少能獲得一塊1x1的巧克力。

我們回顧一下我們之前程式碼的核心部分。

for (int i = 2; ; i++) {
        long long sum = 0; // 這個數可能會溢位int
        for (int j = 0; j < n; j++) {
            sum += (h[j] / i) * (w[j] / i);
        }
        if (sum < k) break;
        else ans++;
    }

這段程式碼是一個二重迴圈,當每塊巧克力的面積都很大,且塊數很多的時候,我們會發現第二層迴圈將被多次執行。

怎樣估計它的耗時呢?我們可以引進一個叫做時間複雜度的概念。它是一個多項式,表示耗時和哪些未知量相關。對於一個未知量,如果兩個未知量可以合併成一個高次未知量,那麼相對低次的就會被忽略。

關於時間複雜度的準確定義,可以參考《高等數學》之類的課本,也可以去看《演算法導論》,裡面會有詳細的介紹。

對於這段程式碼來說,第一個迴圈取決於答案的大小,而答案的大小最大可以達到最大的h或w。這個值詳細地來說,是每一塊巧克力的h和w取最小值m,然後對所有的巧克力取m的最大值。而第二個迴圈取決於n,那麼這段程式碼的時間複雜度就是O(mn)。

我們發現,m最大為1e5,也就是10^5,而n同樣是1e5,那麼這段程式碼可以跑到1e10次計算。

根據第七題我們所講的,一段程式碼1s可以跑1e8次運算。而題目中有如下要求:

資源約定:
峰值記憶體消耗(含虛擬機器) < 256M

CPU消耗  < 1000ms

那麼通過計算,我們很容易發現,這是不滿足CPU消耗的(也就是時間消耗)。

所以我們現在需要想辦法減少它的時間複雜度。

最簡單的方法就是從現有的方法進行改進,那麼是否可行呢?

從優化來講,這兩個迴圈都有優化的可能。我們就來分析一下:

第一重迴圈:迴圈的大小取決於答案的大小。如果我們能快速地確定答案的範圍,這個迴圈的時間複雜度就會減小很多。

第二重迴圈:迴圈的大小取決於n的大小,似乎沒辦法優化,除非想出一種新的方法。

似乎只能優化第一重迴圈。。。怎樣優化呢?

我們發現從小往大計算,會面對答案很大的情況,那麼怎麼確定答案的上界呢?

由於分割的巧克力塊,最大最大也要滿足總面積不超過當前巧克力塊的總面積,所以我們通過巧克力的總面積估計出理論上可以分出的最大巧克力塊的大小。

那麼我們獲得上界之後,怎麼才能保證答案很大或答案很小都不會超時呢?我們同時地從兩頭進行計算即可。

程式碼及執行結果

#include <cstdio>
#include <cmath>
#include <algorithm>
using namespace std;
const int MAX_N = 1e5 + 5;
const double EPS = 1e-8; // 在浮點數運算中,會出現誤差,EPS用於修正這些誤差
long long h[MAX_N], w[MAX_N];
int main() {
    int n, k, max_R = 0;
    long long area = 0; //在每塊巧克力都最大且數量也最大的情況下,area是1e18,並不會溢位
    scanf("%d%d", &n, &k);
    for (int i = 0; i < n; i++) {
        scanf("%lld%lld", &h[i], &w[i]);
        area += h[i] * w[i];
        max_R = max(max_R, (int)min(h[i], w[i]));
    }
    int ans, L = 2, R = min(int(sqrt(area / k + EPS) + EPS), max_R);
    while (true) {
        long long sum = 0;
        for (int j = 0; j < n; j++) {
            sum += (h[j] / L) * (w[j] / L);
        }
        if (sum < k) {
            ans = L - 1;
            break;
        }
        else L++;
        sum = 0;
        for (int j = 0; j < n; j++) {
            sum += (h[j] / R) * (w[j] / R);
        }
        if (sum >= k) {
            ans = R;
            break;
        }
        else R--;
    }
    printf("%d\n", ans);
    return 0;
}

執行結果:


第三種方法

分析

這樣的解法能不能完美的解決這道題呢?實際上並不是。因為可能會有一種情況:答案很大,但是離上界又相距甚遠。儘管這種情況很少見,但依然存在。

那麼我們應該怎樣繼續優化呢?

暫時沒有了= =。。。

那我們能不能找到一種新的方法解決這道題呢?

考慮之前發現的規律:在某一個大小的巧克力塊不滿足時,比它大的情況都不需要考慮;在某一個大小的巧克力塊滿足時,比它小的情況都不需要考慮。

為什麼呢?因為巧克力塊的大小越大,可以被分割出的塊數就越少。

我們在確定答案的時候就可以利用這個規律:

我們在答案的範圍中,尋找它的中間值。如果中間值符合條件,那麼答案就肯定在中間值和上界之間,反之就在下界和中間值之間。

顯然,這樣找到答案,會節省很多時間,相比於之前的兩重迴圈,這種方法只需要O(nlogn)的時間。在任何情況下,這種方法都是可行的。

實際上,這種方法有統一的演算法名稱:整數二分。在一個符合單調性的整數序列中找一個值,都可以用整數二分來完成。

程式碼及執行結果

#include <cstdio>
#include <cmath>
#include <algorithm>
using namespace std;
const int MAX_N = 1e5 + 5;
const double EPS = 1e-8; // 在浮點數運算中,會出現誤差,EPS用於修正這些誤差
long long h[MAX_N], w[MAX_N];
int n, k;
bool judge(int x) {
    long long sum = 0;
    for (int i = 0; i < n; i++) {
        sum += (h[i] / x) * (w[i] / x);
    }
    return sum >= k;
}
// 以下這段程式碼被稱為整數二分
int erfen(int L, int R) {
    int ans = L;
    while (L <= R) {
        int M = (L + R) / 2;
        if (judge(M)) {
            ans = M;
            L = M + 1;
        }
        else R = M - 1;
    }
    return ans;
}
int main() {
    int max_R = 0;
    long long area = 0;
    scanf("%d%d", &n, &k);
    for (int i = 0; i < n; i++) {
        scanf("%lld%lld", &h[i], &w[i]);
        area += h[i] * w[i];
        max_R = max(max_R, (int)min(h[i], w[i]));
    }
    int ans = erfen(2, min(int(sqrt(area / k + EPS) + EPS), max_R));
    printf("%d\n", ans);
    return 0;
}

執行結果:


這樣我們就完美解決這道題啦。

關於整數二分和浮點數精度修正,我們會在以後的部落格中講到=w=