基礎演算法----二分查詢與二分答案淺析
0x04 二分
兩種二分寫法:
int l = 0, r = n + 1;
while(l < r)
{
mid = l + (r - l) / 2;
if(a[mid] >= x) r = mid;
else l = mid + 1;
}
while(l < r)
{
mid = (l + r + 1) << 1;
if(a[mid] <= x) l = mid;
else r = mid - 1;
}
1、迴圈條件 l < r
可以保證最終l == r此時mid一定落在正確答案上
2、l 與 r 的改變
第一種是找出lower_bound(x),大於等於x的最小的數字,當a[mid] >= x,mid之後的數字肯定都不滿足,而mid可能是正確答案,因此r = mid;當a[mid] < x, mid一定不是正確答案,所以l = mid + 1;
第二種是找出小於等於x的最大數字,當a[mid] <= x, mid之前的數字肯定都不滿足,而mid可能是正確答案,因此l = mid;當a[mid] > x, mid一定不是正確答案,所以r = mid - 1;
3、mid的改變 + [0, n+1]
mid = l + (r - l) / 2 可以防止溢位
而mid = (r + l + 1) << 1 使得mid不會等於l,避免當r - l = 1時,mid始終等於l,當進入l = mid語句後形成死迴圈的情況;
而將原本的區間[1, n]擴大到[0, n+1]可以處理無解的情況
總結:具體情況具體分析
1、根據a[mid] 與 目標值的比較結果,選取相應的左右半區間
2、根據要尋找的目標值,判斷mid + 1, mid - 1能否確定不是正確答案,隨後選用配套的兩種二分方案之一:"r = mid, l = mid + 1, mid = l + (r - l) / 2" and "l = mid, r = mid - 1, mid = (l + r + 1) << 1" 中的一種
3、終止條件為l == r,此時該位置為最終答案。
實數域上的二分
1、一般要保留k位小數的時候,取精度eps = 1e-(k+2)
2、while條件為l + eps < r
while(l + eps < r) { double mid = (l + r) / 2; if(calc(mid)) l = mid; else r = mid;
3、若難以確定精度,則利用固定迴圈次數進行二分,往往甚至精度更高
For(i, 1, 100)
{
double mid = (l + r) / 2;
if(calc(mid)) l = mid;
else r = mid;
}
例 aw102前置例題 最大矩陣和--POJ1050
int main ()
{
cin >> n;
For(i, 1, n)
For(j, 1, n) cin >> mat[i][j];
For(i, 1, n)
{
clr(s);
For(j, i, n)
{
For(k, 1, n) s[k] += mat[j][k];
int num = 0;
For(k, 1, n)
{
num += s[k];
if(num <= 0) num = 0;
ans = max(ans, num);
}
}
}
cout << ans << endl;
return 0;
}
遍歷第i行到第j行,同時在不同i的情況下用sum儲存第i行到第j行每一列的和,每儲存完第j行,求一次最大子陣列和,並與ans比較,以此做到了將二維變成了一維,並將所有不同行的矩陣求得了最大子矩陣之和,最終得到最大矩陣之和
例 aw102 最佳牛圍欄
題目大意:求所有長度不小於L的子序列中最大的平均數(子序列之和/子序列長度)
思路分析:實數域二分+字首和
我們可以將求最大平均數 通過將序列每一項減去平均數 轉化為求長度≥L的子序列之和為非負,既然要最大平均數,因此所得到的非負子序列之和越大則越優(因為這段子序列每一項可以減去更大的平均數,依然保證非負),即求能使得序列每一項減去自己後,最大子序列和非負的最大平均數
**解題步驟:1、先用實數域二分求得平均數 **
double l = 0, r = 1e6;
double mid;
double eps = 1e-5; //保留三位小數
while(l + eps < r) //實數域二分
{
mid = (l + r) / 2;
if(check(mid)) l = mid;
else r = mid;
}
2、利用該平均數生成一個新的序列,並求字首和
3、利用字首和思想求新序列的最大子序列之和,並判斷是否≥0
bool check(double mid)
{
For(i, 1, n) s[i] = s[i-1] + a[i] - mid; //步驟2 求新序列的字首和
double minn = 1e6;
for(int i = 0, j = f; j <= n; j++, i++) //利用二指標的思想,先將指標的位置差設為L,隨後每次迴圈同步加1
{
minn = min(minn, s[i]); //minn一定為當前所有S[j]能減的最小字首和,且減去最小的即為最大的子序列之和
if(s[j] - minn >= 0) return true; //符合條件繼續二分求最優解
}
return false;
}
完整Code:
//#pragma comment(linker, "/STACK:10240000000000,10240000000000")
//#pragma GCC optimize(2)
#include <bits/stdc++.h>
#define For(i,a,b) for (int i=(a);i<=(b);++i)
#define Fod(i,b,a) for (int i=(b);i>=(a);--i)
#define mls multiset
#define lb lower_bound
#define ub upper_bound
#define pb push_back
#define pob pop_back
#define itt iterator
#define clr(x) memset(x, 0, sizeof(x));
typedef long long ll;
typedef unsigned long long ull;
using namespace std;
const int MAXN = 0x7fffffff;
int n, f;
double a[100005];
double b[100005];
double s[100005];
bool check(double mid)
{
For(i, 1, n) s[i] = s[i-1] + a[i] - mid;
double minn = 1e6;
for(int i = 0, j = f; j <= n; j++, i++)
{
minn = min(minn, s[i]);
if(s[j] - minn >= 0) return true;
}
return false;
}
int main ()
{
cin >> n >> f;
For(i, 1, n) cin >> a[i];
double l = 0, r = 1e6;
double mid;
double eps = 1e-5;
while(l + eps < r)
{
mid = (l + r) / 2;
if(check(mid)) l = mid;
else r = mid;
}
cout << (int)(r*1000) << endl;
return 0;
}