[學習筆記]二分與分治


二分

二分法常用來查找單調序列單調函數上的答案.

當問題的答案具有單調性時,可以考慮通過二分求解.

先思考一個簡單問題

A心里想一個1-1000之間的數,B來猜,B可以問問題,A只能回答是或者不是,怎么猜才能問的問題次數最小?

  • 是1嗎?是2嗎?……平均要問500次
  • 大於500嗎?大於750嗎?大於625嗎?……每次縮小猜測范圍到上次的一半,只需要10次(\(log_21000\))

這就是二分法的一個簡單運用.

二分的實現方法有很多種,對於整數集合上的二分,需要注意終止邊界,左右區間取舍時的開閉,避免漏掉答案或造成死循環;對於實數域上的二分,需要注意精度的取舍.

這里推薦大家采用<算法進階指南>上的二分寫法.

整數集合上的二分

下面代碼摘自<算法進階指南>P24  親測好用,比賽時一直采用這種二分寫法.

  • 在單調遞增序列\(a\)中查找$\geq x $的數中最小的一個
  while(l<r){
      int mid = (l+r)>>1;/*右移運算 相當於除2並且向下取整*/
      if(a[mid]>=x) r=mid;
      else l=mid+1;
  }
  return a[l];
  • 在單調遞增序列\(a\)中查找\(\leq x\)的數中最大的一個
  while(l<r){
      int mid = (l+r+1)>>1;
      if(a[mid]<=x) l=mid;
      else r=mid-1;
  }
  return a[l];

注意,這里的單調是廣義的單調,如果一個數組中的左側或者右側都滿足某一種條件,而另一側都不滿足這種條件,也可以看作是一種單調,比如把滿足條件看做1不滿足看做 0.故這里的單調序列也可以換成單調函數,下面會詳細解釋.

使用上面代碼的時候要注意考慮問題到底屬於哪種情況.

上面模板二分結束條件恰好位於答案所處的位置,不需要再額外判斷.希望同學們能采用這種寫法,少走一些彎路.

先完成一個模板題

例題A:二分查找

STL的二分查找

對於一個有序的 array 你可以使用 std::lower_bound() 來找到第一個大於等於你的值的數, std::upper_bound() 來找到第一個大於你的值的數。

如:

int l = std::lower_bound(a+1,a+1+n,x)-a;
return a[l];

詳細內容后面的講座會詳細介紹,這里不再贅述.

如果自學會了,可以嘗試用STL解決例題A,巧用STL可以減少很多代碼量.

實數域上的二分

有關實數的題一般都會給出精度要求,只要在題目允許的精度范圍內就是對的.

所以需要確定好所需的精度\(eps\),太小會超時,太大又不符合要求.

一般\(eps\)取1e-6或者1e-7 根據題目要求靈活變化

對於精度的問題,可以看這篇博客:https://www.cnblogs.com/oyking/p/3959905.html

while(l + 1e-6 <r){
    double mid=(l+r)/2;/*這里不能再用右移運算了*/
    if(calc(mid))r=mid;//一般運用於單調函數上
    else l=mid;
}

二分+check

二分的作用不只是查找某個值,在算法競賽中二分常常與其他算法相結合.最廣泛的用途還是解決單調函數的相關問題.

標題的check可以理解為函數,返回值為bool類型.(即0或者1)

0代表不符合條件,1代表符合條件.

當函數的返回值隨着x的變化呈現單調性,如:0 0 0 0 0 1 1 1 這樣的序列

可以采用二分解決.通過二分可以找出滿足條件最小位置,像上面的序列答案就是6.

與單調函數一樣有實數域和整數域之分:

例題B:木材加工

木材廠有一些原木,現在想把這些木頭切割成一些長度相同的小段木頭,需要得到的小段的數目是給定了。當然,我們希望得到的小段越長越好,你的任務是計算能夠得到的小段木頭的最大長度。
木頭長度的單位是厘米。原木的長度都是正整數,我們要求切割得到的小段木頭的長度也要求是正整數。

#include <bits/stdc++.h>
const int maxn = 1e4 + 5;
int s[maxn], n, k;

bool check(int x) {
    int res = 0;
    for (int i = 1; i <= n; ++i) {
        res += s[i] / x;
        if (res >= k) return 1;
    }
    return 0;
}

int main() {
    scanf("%d%d", &n, &k);
    int ans = 0;
    for (int i = 1; i <= n; ++i) {
        scanf("%d", &s[i]);
        ans += s[i];
    }
    int r = ans / k;//上限
    int l = 0;
    while (l < r) {//相當於在1 1 1 0 0 0 這種序列找最大的1
        int mid = (l + r + 1) >> 1;
        if (check(mid)) {
            l = mid;
        } else {
            r = mid - 1;
        }
    }
    printf("%d\n", l);
    return 0;
}

例題C: POJ1064

最大化最小值

在做題過程中如果有讓最小值最大,讓最大值最小這種問題

十有八九需要二分.

什么叫最大化最小值問題呢?看下面的例題就知道了:

例題D:跳石頭

http://icpc.upc.edu.cn/problem.php?id=1752

輸入輸出樣例 1 說明:將與起點距離為 2 和 14 的兩個岩石移走后,最短的跳躍距離為 4(從與起點距離 17 的岩石跳到距離 21 的岩石,或者從距離 21 的岩石跳到終點)。

另:對於 20%的數據,0 ≤ M ≤ N ≤ 10。 對於50%的數據,0 ≤ M ≤ N ≤ 100。

對於 100%的數據,0 ≤ M ≤ N ≤ 50,000,1 ≤ L ≤ 1,000,000,000。

令check(x):最小距離不小於x是否滿足題意.

check(x)值大致為 1 1 1 0 0 0 故選取第二種二分方式

#include <bits/stdc++.h>
const int maxn = 5e4+10;
int s[maxn],n,m;
bool check(int k){
    int ans=0,last=0;
    for(int i=1;i<=n;++i){
        if(s[i]-s[last]>=k){
            last=i;
        }
        else ans++;
    }
    if(s[n+1]-s[last]<k) return 0;
    return ans<=m;
}
int main(){
    int l;
    std::cin>>l>>n>>m;
    for(int i=1;i<=n;++i){
        std::cin>>s[i];
    }
    s[n+1]=l;
    int L=0,R=l+10;
    while(L<R){
        int mid=(L+R+1)>>1;
        if(check(mid)){
            L=mid;
        }else{
            R=mid-1;
        }
    }
   std::cout<<l;
}

最大化平均值

\(n\)個物品的重量和價值分別是\(w_i\)\(v_i\),從中選出\(k\)個物品使得單位重量價值最大。

假設我們選的物品集合為S,那么他們的單位重量價值為

\[\frac{\sum_{i\in S}{v_i}}{\sum_{i\in S}{w_i}} \]

我們是不是可以貪心地從大到小選取\(\frac{v_i}{w_i}\)最大的前k個呢

很明顯不可以:

\[\frac{\sum_{i\in S}{v_i}}{\sum_{i\in S}{w_i}}\neq \sum_{i\in S} \frac{v_i}{w_i} \]

令check(x):可以選擇使得單位重量的價值不小於x

\[\frac{\sum_{i\in S}{v_i}}{\sum_{i\in S}{w_i}} \geq x \]

很明顯x越小越容易滿足

現在就是怎么check的問題

所以我們轉換一下

\[\sum_{i\in S}{v_i-x*w_i}\geq 0 \]

由(3)易得,我們可以對(3)進行排序貪心地選取.

因此變成

\[check(x)=(\sum_{i\in S}{v_i-x*w_i}從小到大前k個的\geq0) \]

因為平均值是浮點型,所以需要采用實數域上的二分

int n, k;
int w[550], v[550];
double y[550];

bool check(double x) {
    for (int i = 0; i < n; ++i) {
        y[i] = v[i] - x * w[i];
    }
    std::sort(y, y + n);
    double sum = 0;
    for (int i = 0; i < k; ++i) {
        sum += y[n - i - 1];
    }
    return sum >= 0;
}
void erfen(){
    double l=0,r=1e6+10;
    while(l + 1e-6 <r){
        double mid=(l+r)/2;
        if(check(mid))r=mid;
        else l=mid;
    }
    std::cout<<l<<std::endl;
}

分治

分治法通過將問題划分為規模最小的子問題,遞歸地解決划分后的子問題,再將結果合並從而高效地解決問題.

復雜度一般為\(log\)級別

分治法運用很多,有些問題比較復雜,這里只介紹數列上的分治.

分治算法可以分三步走:分解 -> 解決(觸底) -> 合並(回溯)

  1. 分解原問題為結構相同的子問題。
  2. 分解到某個容易求解的邊界之后,進行遞歸求解。
  3. 將子問題的解合並成原問題的解。

解決分治最重要的一點就是只要明確每個函數能做什么,千萬不能試圖跳進細節.不然腦子會炸的.

歸並排序

歸並排序是分治法最典型的運用.

兩個有序數列合並的時間復雜度是O(n),n為數列長度大小.

將數列每次划分成兩半,有log層

(下圖是以后要學的重要數據結構 線段樹)

所以總的時間復雜度是O(nlogn)

點擊查看源網頁

void Merge(int l, int mid, int r) {
    int i = l, j = mid + 1;
    for (int k = l; k <= r; ++k) {
        if (j > r || (i <= mid && a[i] < a[j])) b[k] = a[i++];
        else b[k] = a[j++];
    }
    for (int k = l; k <= r; ++k) a[k] = b[k];
}

void Sort(int l, int r) {
    if (l == r) return;
    int mid = (l + r) >> 1;
    Sort(l, mid);
    Sort(mid + 1, r);
    Merge(l, mid, r);
}

歸並排序的結構是:

void merge_sort(一個數組) {
  if (可以很容易處理) return;
  merge_sort(左半個數組);
  merge_sort(右半個數組);
  merge(左半個數組, 右半個數組);
}

分形

分形是一類很好玩的題,代碼簡單,但是需要一些思維量

要用到數學歸納的思想,並且需要找到一些規律.

在小范圍考慮細節,大范圍從整體上思考

例題:Windows Of CCPC

中國大學生程序設計競賽簡稱CCPC
CCPC組委會設計了一個名為CCPC Windows的圖標。圖中的一級CCPC Windows如圖所示:

img

二級CCPC窗口CCPC Windows如圖所示:

img

我們可以清楚地看到,二級圖可由四個一級圖變化而來,左上,右上還有右下都是一個一級圖,左下圖是將一級圖的'C'改成了'P', 'P'改成了'C'。
三級圖也是這樣由二級圖得到的,四級圖、五級圖.....同理。
現在,請你輸出第k級圖的樣子。

從細節上考慮:左下角的字符與其他都不同

從整體上考慮:若按照2的冪次划分,左下塊的字符與其他都不同

#include <bits/stdc++.h>

int s[2000][2000];

void draw(int x, int y, int st1, int st2, int len) {
    if (len == 0) {
        s[x][y] = st1;
        return;
    }
    draw(x, y, st1, st2, len >> 1);
    draw(x, y + len, st1, st2, len >> 1);
    draw(x + len, y, st2, st1, len >> 1);
    draw(x + len, y + len, st1, st2, len >> 1);
}

int main() {
    int n, _;
    std::cin >> _;
    while (_--) {
        std::cin >> n;
        draw(1, 1, 1, 2, 1 << n);
        int len = 1 << n;
        for (int i = 1; i <= len; ++i) {
            for (int j = 1; j <= len; ++j) {
                if (s[i][j] == 1) {
                    std::cout << "C";
                } else {
                    std::cout << "P";
                }
            }
            std::cout << std::endl;
        }
    }
    return 0;
}

拓展題

題意:求\(A^B\)的所有約數之和mod9901

知識點:約數和定理[數論]
大於1的正整數:

\[\prod_{i=1}^kp_i^{c_i}=p_1^{c_1}*p_2^{c_2}*....p_k^{c_k} \]

正約數的個數:

\[\prod_{i=1}^k(c_i+1)=(c_1+1)*(c_2+1)*....*(c_k+1) \]

約數和:

\[\prod_{i=1}^k(\sum_{j=0}^{c_i}(p_i)^j)=(1+p^1+p^2+..+p^{c_1})*...*(1+p_k+p_k^2+..+p_k^{c_k}) \]

這題用到約數和 就是把\(c_i*B\)就好

先分解質因數求出\(p_i\)\(c_i\)

約數和通過等比數列求和來做

等比數列求和可以通過分治遞歸的方法

將求和分成兩部分:當n為偶數時

\[1+p^1+p^2+..p^{n/2-1}+p^{n/2}*(1+p^1+p^2+..+p^{n/2-1})+p^{n/2} \]

\[\sum_{i=0}^{n/2-1}p^i+p^{n/2}*\sum_{i=0}^{n/2-1}p^i+p^{n/2} \]

每次遞歸之后 問題規模都會縮小一半 

#include <iostream>
#define ll long long
using namespace std;
const int mod=9901;
const int maxn=1e4;
ll p[maxn],c[maxn],pos;
ll qpow(ll a,ll n){//快速冪
    ll res=1;
    a%=mod;
    while(n){
        if(n&1) res=res*a%mod;
        a=a*a%mod;
        n>>=1;
    }
    return res;
}
ll solve(ll k,ll n){//等比數列求和
        if(n==0) return 1;
        if(n&1){
            ll res=solve(k,n/2);
            return (res+res*qpow(k,n/2+1)%mod)%mod;
        }
        else{
            ll res=solve(k,n/2-1);
            return ((res+res*qpow(k,n/2)%mod)%mod+qpow(k,n))%mod;
        }
}
void divide(ll n){//分解質因數
    for(ll i=2;i*i<=n;++i){
        if(n%i==0){
            pos++;
            p[pos]=i;
            while(n%i==0)n/=i,c[pos]++;
        }
    }
    if(n>1) p[++pos]=n,c[pos]=1;
}
int main(){
    ll n,k,ans=1;
    ios::sync_with_stdio(false);
    cin.tie(0);
    cin>>n>>k;
    divide(n);
    for(int i=1;i<=pos;++i){
        ans=ans*solve(p[i],c[i]*k)%mod;
    }
    cout<<ans<<endl;
    return 0;
}

總結

二分與分治其實都算是一種思想,單純的模板考題很少.

一般都是和其他知識組合在一塊

本文中題目的參考代碼盡量先自己思考之后再參考.

因為最近忙於一大堆事,寫得比較倉促,忘諒解.

最后希望大家喜歡算法,愛上編程.

參考資料

[1]oi wiki https://oi-wiki.org/intro/icpc/

[2]算法(第四版)

[3]算法競賽進階指南

[4]挑戰程序設計競賽


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM