二分算法
查找流程
示例
代码演示
#include <iostream>
using namespace std;
void output(int *arr, int n, int ind = -1) {
int len = 0;
for (int i = 0; i < n; i++) {
len += printf("%4d", i);
}
printf("\n");
for (int i = 0; i < len; i++) printf("-");
printf("\n");
for (int i = 0; i < n; i++) {
if (i == ind) printf("\033[1;32m");
printf("%4d", arr[i]);
if (i == ind) printf("\033[0m");
}
printf("\n");
return ;
}
int binary_search(int *arr, int n, int x) {
int head = 0, tail = n - 1, mid;
while (head <= tail) {
mid = (head + tail) / 2;
printf("[%d, %d], mid = %d, arr[%d] = %d\n",
head, tail, mid,
mid, arr[mid]
);
if (arr[mid] == x) return mid;
if (arr[mid] < x) head = mid + 1;
else tail = mid - 1;
}
return -1;
}
void test_binary_search(int n) {
int *arr = (int *)malloc(sizeof(int) * n);
arr[0] = rand() % 10;
for (int i = 1; i < n; i++) arr[i] = arr[i - 1] + rand() % 10;
output(arr, n);
int x;
while (~scanf("%d", &x)) {
if (x == -1) break;
int ind = binary_search(arr, n, x);
output(arr, n, ind);
}
free(arr);
return ;
}
int main() {
#define MAX_N 10
test_binary_search(MAX_N);
return 0;
}
泛型情况
分别为前0后1和前1后0两种情况。第一种情况要求我们找到第一个1的位置,第二种情况要求我们找到最后一个1的位置。
情况1的处理策略:
当arr[mid]=0时,head=mid+1;
当arr[mid]=1时,tail=mid;
当head=tail时,返回head(tail)的值。
(mid=(head+tail)/2)
情况2的处理策略:
当arr[mid]=0时,tail=mid-1;
当arr[mid]=1时,head=mid;
当head=tail时,返回head(tail)的值。
(mid=(head+tail+1)/2) 情况2的特殊处理!!!
例题
1.搜索插入位置
给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
请必须使用时间复杂度为 O(log n) 的算法。
示例 1:
输入:nums = [1,3,5,6], target = 5
输出: 2
示例 2:
输入: nums = [1,3,5,6], target = 2
输出: 1
示例 3:
输入: nums = [1,3,5,6], target = 7
输出: 4
思路:本题实际上是泛型情况中所提到的前0后1的情况(也可以是前1后0),即前面的若干元素小于x,后面的若干元素大于等于x,我们要找到第一个大于等于x的位置。
代码实现:
class Solution {
public:
int searchInsert(vector<int>& nums, int target) {
int head=0,tail=nums.size(),mid;
//注意tail设置成了n,因为要覆盖所以合法的插入位置
while(head<tail)
{
mid=(head+tail)/2;
if(nums[mid]<target)head=mid+1;
else tail=mid;
}
return head;
}
};
2.无重复字符的最长子串
给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串 的长度。
示例 1:
输入: s = “abcabcbb”
输出: 3
解释: 因为无重复字符的最长子串是 “abc”,所以其长度为 3。
示例 2:
输入: s = “bbbbb”
输出: 1
解释: 因为无重复字符的最长子串是 “b”,所以其长度为 1。
示例 3:
输入: s = “pwwkew”
输出: 3
解释: 因为无重复字符的最长子串是 “wke”,所以其长度为 3。
请注意,你的答案必须是 子串 的长度,“pwke” 是一个子序列,不是子串。
思路:当本问题抽象成1111100000模型,将能找到的子串长度进行二分,在其中找最大的。
代码实现
class Solution {
public:
int check(string s,int l)
{
int vis[256]={0};
int k=0;
for(int i=0;i<s.size();i++)
{
vis[s[i]]++;
if(vis[s[i]]==1)k++;
if(i>=l)
{
vis[s[i-l]]--;
if(vis[s[i-l]]==0)k--;
}
if(l==k)return 1;
}
return 0;
}
int lengthOfLongestSubstring(string s) {
int head=0,tail=s.size(),mid;
while(head<tail)
{
mid=(head+tail+1)/2;
if(check(s,mid))head=mid;
else tail=mid-1;
}
return head;
}
};
3.寻找两个正序数组的中位数
给定两个大小分别为 m 和 n 的正序(从小到大)数组 nums1 和 nums2。请你找出并返回这两个正序数组的 中位数 。
算法的时间复杂度应该为 O(log (m+n)) 。
示例 1:
输入:nums1 = [1,3], nums2 = [2]
输出:2.00000
解释:合并数组 = [1,2,3] ,中位数 2
示例 2:
输入:nums1 = [1,2], nums2 = [3,4]
输出:2.50000
解释:合并数组 = [1,2,3,4] ,中位数 (2 + 3) / 2 = 2.5
思路:如果将本问题泛化为找第K的元素的值的话,那么可以这么处理:每次在两个数组中分别找k/2个数,比较最后一位的大小,如果nums1[k/2]的值小于nums2[k/2]的话,那么第k个元素一定不在nums1[k/2]及之前,此时就在剩余的位置里面找第k/2大的数,如此递归。
代码实现
class Solution {
public:
int findK(vector<int>nums1,int ind1,vector<int>nums2,int ind2,int k)
{
if(nums1.size()==ind1)return nums2[ind2+k-1];
if(nums2.size()==ind2)return nums1[ind1+k-1];
if(k==1)return min(nums1[ind1],nums2[ind2]);
int n=nums1.size(),m=nums2.size();
int cnt1=min(k/2,n-ind1);
int cnt2=min(m-ind2,k-cnt1);
cnt1=k-cnt2;
if(nums1[cnt1+ind1-1]<nums2[cnt2+ind2-1])
{
return findK(nums1,ind1+cnt1,nums2,ind2,k-cnt1);
}
else return findK(nums1,ind1,nums2,ind2+cnt2,k-cnt2);
}
double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
int n=nums1.size(),m=nums2.size();
if((n+m)%2==1)return findK(nums1,0,nums2,0,(n+m)/2+1);
else
{
double a=findK(nums1,0,nums2,0,(n+m)/2);
double b=findK(nums1,0,nums2,0,(n+m)/2+1);
return (a+b)/2;
}
}
};
4.最大平均值
题目描述
给定一个有 N 个元素的非负序列,求长度大于等于 M 的连续子序列的最大平均值。
输入
第一行输入两个数 N,M。(1≤N,M≤100000)
接下来 N 行,每行输入一个数表示非负序列。
输出
输出一个整数表示最大平均值乘 1000 的结果。
样例输入
10 6
6
4
2
10
3
8
5
9
4
1
样例输出
6500
思路
1.将问题进行等价转换
1.是否存在一段长度>=M的序列,平均值>=A(求平均值自然可以想到求和值,使用前缀和数组来维护)---->
2.是否存在一段长度>=M的序列,和值>=A×L(由于L是变量,因此每个长度下A×L的值都不同,继续对问题进行转化)---->
3.是否存在一段长度>=M的序列,和值>=0(原始序列的每一项都减去A)
2.使用1111100000型的二分模型
在0到max_val区间内进行二分,找到最后一个1所在的位置。
代码实现
#include<iostream>
using namespace std;
#define MAXSIZE 100000
long long arr[MAXSIZE+5],sum[MAXSIZE+5];
int N,M;
bool check(int item)
//由于要调用多次check,注意处理的过程中不要改变arr的值
{
for(int i=1;i<=N;i++)
sum[i]=arr[i]-item;
sum[0]=0;
for(int i=1;i<=N;i++)
sum[i]+=sum[i-1];
long long ret=0;
for(int i=M;i<=N;i++)
{
ret=min(ret,sum[i-M]);
if(sum[i]-ret>=0)return 1;
}
return 0;
}
int main()
{
cin>>N>>M;
arr[0]=0;
long long max_val=0;
for(int i=1;i<=N;i++)
{
scanf("%d",&arr[i]);
arr[i]*=1000;
max_val=max(max_val,arr[i]);//找到最大值
}
int head=0,tail=max_val,mid;
while(head<tail)//1111100000
{
mid=(head+tail+1)/2;
if(check(mid))head=mid;
else tail=mid-1;
}
cout<<head;
return 0;
}
5.奶牛围栏
题目描述
约翰打算建一个围栏来圈养他的奶牛。作为最挑剔的兽类,奶牛们要求这个围栏必须是正方形的,而且围栏里至少要有 C (1≤C≤500) 个草场,来供应她们的午餐。
约翰的土地上共有 N (C≤N≤500) 个草场,每个草场在一块1x1的方格内,而且这个方格的 坐标不会超过 10000。有时候,会有多个草场在同一个方格内,那他们的坐标就会相同。
现求围栏的最小边长为多少。
输入
第一行输入两个数 C,N。
接下来 N 行每行两个数,表示每个草场的坐标 Xi,Yi。
输出
输出围栏的最小边长。
样例输入
3 4
1 2
2 1
4 1
5 2
样例输出
4
思路
本题为0000011111二分模型,找到第一个1,即符合要求的最小边长。
有了最外层的二分框架,如何去实现check函数?本题使用二维扫描线法,即先找到x符合要求的一组数,再判断这一组数中是否有足够数量的元素在y上也符合要求。
代码实现
#include<iostream>
#include<algorithm>
using namespace std;
#define MAXSIZE 500
int C,N;
struct Data{
int x,y;
}arr[MAXSIZE+5];
int temp[MAXSIZE+5];
bool cmp(const Data&a,const Data&b)
{
return a.x<b.x;
}
bool check_y(int a,int b,int l)
{
int cnt=0;
for(int i=a;i<=b;i++)
temp[++cnt]=arr[i].y;
sort(temp+1,temp+1+cnt);
for(int i=C;i<=cnt;i++)
{
if(temp[i]-temp[i-C+1]<l)return 1;
}
return 0;
}
bool check(int l)
{
for(int i=1,j=1;j<=N;j++)
{
while(arr[j].x-arr[i].x>=l)i++;
if(j-i+1<C)continue;
if(check_y(i,j,l))return 1;
}
return 0;
}
int main()
{
cin>>C>>N;
for(int i=1;i<=N;i++)
cin>>arr[i].x>>arr[i].y;
sort(arr+1,arr+1+N,cmp);
int head=1,tail=10000,mid;
while(head<tail)
{
mid=(head+tail)/2;
if(check(mid))tail=mid;
else head=mid+1;
}
cout<<head;
return 0;
}