程序设计课 期末考试复习 专题二:二分

第二个专题是二分算法。

基本用法

二分算法就是利用每次对有序的数组进行折半判断,进行查找某一特定元素(该元素可能直接是给出的数据,也有可能是二分搜索要求得的答案)。

时间复杂度:O(logn)。

对于普通的一维数组的进行某一规定元素的查找的基本算法

int binarySearch(int a[],int n,int k)
{
	int l=0,r=n-1;
	while(l<=r)
	{
		int mid=(l+r)/2;
		if(a[mid]==k)
		    return mid;
		else if(a[mid]>k)
		    r=mid-1;
		else l=mid+1;
	}
	return -1;
}

上面的写法是最普遍的写法,如果数组中有连续的要查找的元素,上面的算法不会关心这些连续元素的位置,只要找到该元素就会返回找到那个元素的下标。例如:查找元素 5 ,从5 5 5 5 6 7 8 9 10 11,这样返回的下标就是 2 。

以下两种简便的写法,可以返回符合连续元素最小的或者最大的下标。

返回要查找的连续元素的最小坐标

这里注意要用ans记录中间的查找到的下标,而如果用mid=(l+r+1)/2,r=mid,或者同时不用0下标;或者其他方式来避免各种死循环也是不行的。多试试例子就行(比如:5 5 5 6 7 查找5;1 2 3 5 5 查找5;等等)。所以不如直接利用ans记录中间查找到的下标,同时更改r和l时不再考虑mid,这样就可以完全避免了死循环。(在求符合结果的最大坐标时也是利用这种方法)

int binarySearch1(int a[],int n,int k)
{
	int l=0,r=n-1,ans=-1;
	while(l<=r)
	{
		int mid=(l+r)/2;
		if(a[mid]>=k)
		    r=mid-1,ans=mid;
		else l=mid+1;
	}
	return ans;
}

返回要查找的连续元素的最大坐标

int binarySearch2(int a[],int n,int k)
{
	int l=0,r=n-1,ans=-1;
	while(l<=r)
	{
		int mid=(l+r)/2;
		if(a[mid]<=k)
		    l=mid+1,ans=mid;
		else r=mid-1;
	}
	return ans;
}

以前我对二分算法的了解就仅限于最普遍的查找算法,但是二分算法的使用的地方远不止搜索一维数组中的特定元素。

上面就是二分简单的思想和应用。

而二分的广义思想:将一个规模为n的问题分解成k个(一般是2个)规模较小的子问题,这些子问题相互独立且与原问题性质相同。求出子问题的解,再通过子问题的解得出原问题的解。它属于分支算法。

拓展用法

1.求方程的根

一般这类问题就是给定一个方程式,例如:x^3+x^2+x+23=0。求这个方程式的解(或者规定某一范围),并且规定小数点后精确的位数。

实例:x^6+x-2014=0的在

0~正无穷 的一个解,小数点后面保留5位小数。

题解:首先要判断,函数表达式在这个范围内的单调性,因为要用二分搜索方法进行查找结果。经判断是单调递增的函数,那么在二分查找结果时就要设定判断函数,f(mid)*f(r)<=0:解就在 mid~r 之间;否则就在 l~mid 之间。这样就可以修改左边界和右边界了。另外在做这种题时,最好先自己判断,设定一个大体范围。比如 [1,10]

代码:

#include<iostream>
#include<cstdio>
#include<cmath>
using namespace std;
const double EPS=1e-8;

double f(double x)
{
	return x*x*x*x*x*x + x - 2014;
}

int main()
{
	double l=1,r=10,mid;
	while(abs(r-l)>EPS)
	{
		mid=(l+r)/2;
		if(f(r)*f(mid)<=0)
		    l=mid;
		else r=mid;
	}
	printf("%.5f\n",l);
	return 0;
}

上面的题目的精度精确到了小数点后8位,然后只输出5位。得到的结果是3.55262。实际计算一下发现结果相差在10的-7次方的数量级。证明结果正确。

2.二分答案(最小值最大或最大值最小)

(1)有n条电缆,他们长度分别是len[i]。如果从这n条电缆中切割出k条长度相同的电缆,求这k条电缆的长度最长为多少?

例如: n:5 ;k:5 ;len[i] 分别为:2 5 8 3 4,那么切得长度为:3。 又如:n:4;k:11;len[i] 分别是:8 7 4 5,那么切得长度为:2。

题解:乍一看这个题目,并不是常规二分算法能够使用的模型。但是这样考虑:我们要求一个能够分成那么多段的最大值,如果小于这个值的话肯定能够分成那么多段,如果大于这个值的话,肯定不能分成那么多段。这就好像是我们在使用二分算法的时候使用的那个改变边界值时的判断条件。那么如果我们用二分的思想,对于我们要求的结果,先定一个范围,然后每次将这个范围二分一下,判断中间值是否符合分割的条件,然后不断改变边界值来完成二分查找。这里有两个问题:一、改变边界值的判断条件;二、什么时候停止改变得到正确答案。

这两个问题解决后就可以,按照二分的格式来写出代码来:

#include<iostream>
using namespace std;
int k,n;
int len[10005];

bool can(int x)
{
	int num=0;
	for(int i=0;i<n;i++)
	{
		num+=len[i]/x;
	}
	return num>=k;
}

int find(int left,int right)
{
	int l=left,r=right,ans=0;
	while(l<=r)
	{
		int mid=(l+r)/2;
		if(can(mid))
		{
		    l=mid+1;ans=mid;	
		}
		else r=mid-1;
	}
	return ans;
}

int main()
{
	cin>>n>>k;
	int max=0;
	for(int i=0;i<n;i++)
	{
		cin>>len[i];
		max=max>len[i]?max:len[i];
	}
	int ans=find(0,max);
	cout<<ans<<endl;
	return 0;
}

can函数解决是否可以分成那么多,find函数表明l>r时停止搜索,所以这个程序的复杂度也是为O(logn)。

(2)Aggressive cows(poj2456)

题意:有n个牛舍,分别在x[i]的位置上,然后又m头牛,让它们每只之间尽可能离得远。

题解:二分距离,判断是否每一头牛之间是否符合这个距离的,取二分结果最大值。

AC代码:

#include<iostream>
#include<algorithm>
using namespace std;

int n,m;
int x[100005];

bool can(int dist)
{
	int num=1;
	int cur=0;
	for(int i=0;i<n;i++)
	{
		if(x[i]-x[cur]>=dist)
		{
			cur=i;
			num++;
		}
	}
	return num>=m;
}

int maxdist()
{
	int l=1,r=x[n-1]-x[0],mid,ans=0;
	while(l<=r)
	{
		mid=(l+r)/2;
		if(can(mid))
		{
			ans=mid;l=mid+1;
		}
		else r=mid-1;
	}
	return ans;
}

int main()
{
	cin>>n>>m;
	for(int i=0;i<n;i++)
	{
		cin>>x[i];
	}
	sort(x,x+n);
	int ans=maxdist();
	cout<<ans<<endl;
	return 0;
}

(3)有n个物品,每个物品都有一个重量w和价值v,现在让你从这n个物品中选取k个,然后得到总体的单位价值最大(即k个物品总价值除以总重量)。

题解:这个问题不可以用贪心算法来直接选取前k个单位质量最大的物品。这样得不到正确结果。我们要用二分法,选择二分这个中体单位价值。是否满足的条件是:这个总体的单位价值加入为m,那么对于其中选取的k个物品,每一个物品的价值应该是w[i]*m,然后将这k个物品按照它们应该的价值求出总价值,再按照每一个的实际价值求和得出总价值,如果实际总价值高于应该总价值,那么这个单位价值m合格。如此来二分这个最大单位价值,求得最终结果

 代码:

#include<iostream>
#include<algorithm>
#include<cmath>
using namespace std;
const double EPS=1e-6;
int n,k;
double w[100005],v[100005];
double c[100005];

bool can(double x)
{
	for(int i=0;i<n;i++)
	{
		c[i]=v[i]-x*w[i];
	}
	sort(c,c+n);
	double p=0;
	for(int i=n-1;i>n-1-k;i--)
	    p+=c[i];
	return p>0;
}

double findmax(double left,double right)
{
	double l=left,r=right,mid,ans=0;
	while(abs(r-l)>EPS)
	{
		mid=(l+r)/2;
		if(can(mid))
		{
			ans=mid;l=mid;
		}
		else r=mid;
	}
	return ans;
}
int main()
{
	cin>>n>>k;
	double maxv=0;
	for(int i=0;i<n;i++)
	{
		cin>>w[i]>>v[i];
		maxv=maxv>v[i]?maxv:v[i];
	}
	double ans=findmax(0.0,maxv);
	cout<<ans<<endl;
	return 0;
}

(4)Copying Books(poj1505)

Sample Input

2
9 3
100 200 300 400 500 600 700 800 900
5 4
100 100 100 100 100

Sample Output

100 200 300 400 500 / 600 700 / 800 900
100 / 100 / 100 / 100 100

多组数据。

题解:二分页数,从每本书的页数最大值,到所有书的页数之和。判断函数can要求对于是否满足m人的要求能够导致右边界的移动,从而求出符合要求的连续的值的最大值。然后在while循环里面每次是右边界的移动。

代码:

#include<iostream>
#include<cstdlib>
#include<algorithm>
#include<cstdio>
#include<cstring>
using namespace std;
typedef long long ll;
const ll N = 505;

ll min1, max1;
ll num[N];
bool p[N];
int m, n;

bool can(ll x)
{
	int k = 1;
	ll sum = 0;
	for (int i = 0; i < n; i++)
	{
		sum+=num[i];
		if(sum>x)
		{
			k++;sum=num[i];
		}
	}
	return k<=m; //注意!!! 
}
ll minmax()
{
	ll x = min1;
	ll y = max1;
	ll mid,ans=0;
	while (x <= y)
	{
		mid = (x + y) / 2;
		if (can(mid))
		{
			y = mid-1,ans=mid;   //注意!! 
		}
		else
			x = mid + 1;
	}
	return ans;
}
int main()
{
	int t;
	cin >> t;
	while (t--)
	{
		min1 = -1;
		max1 = 0;
		cin >> n >> m;
		for (int i = 0; i < n; i++)
		{
			cin >> num[i];
			if (min1 < num[i])
				min1 = num[i];
			max1 += num[i];
		}
		memset(p, 0, sizeof(p));
		ll ans = minmax();
		ll sum = 0, k = 0;
		for (int i = n - 1; i >= 0; i--)
		{
			if (sum + num[i] <= ans)
				sum += num[i];
			else
			{
				sum = num[i];
				k++;
				p[i] = true;
			}
		}
for (int i = 0; i < n&&k < m - 1; i++)
		{
			if (!p[i])
			{
				p[i] = true;
				k++;
			}
		}
		for (int i = 0; i < n-1; i++)
		{
			cout << num[i] << " ";
			if (p[i])
			{
				cout << "/ ";
			}
		}
		cout << num[n - 1] << endl;
	}

	return 0;
}

以上是我对二分算法的总结和一些用法的总结。

二分算法主要用处有两方面:首先是对有序数组中查找某一特定元素;另一个是问题的解在一个区间内,通过对这个区间的二分查找,找出答案,要注意求答案时的要求,符合的答案是否有一个连续的区间,取这个区间里最大值还是最小值。

发布了6 篇原创文章 · 获赞 0 · 访问量 184

猜你喜欢

转载自blog.csdn.net/morning_zs202/article/details/92364209