1. 程式人生 > 其它 >基礎演算法----二分查詢與二分答案淺析

基礎演算法----二分查詢與二分答案淺析

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 最佳牛圍欄

題目連結:102. 最佳牛圍欄 - AcWing題庫

題目大意:求所有長度不小於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;
}