话说十个二分九个错,二分不写不知道,一写吓一跳。
解二分的重点还是思维比较重要,当然还有一个重要的就是,二分别写挂了。
给出数字n,k,求得最小的数字v使得:
数据范围: 1≤n≤109,2≤k≤10
对于式子中的下取整部分可以暴力求和,复杂度为 logv 。如何求解v呢?不难发现式子满足一致性:即如果v取小了,式子之和是小于等于n的;如果取大了,那么就会大于等于n。并且如果和值大了就应该减小v的取值,和值小了应该增大v的取值。 这恰恰和二分的思想是一样的,由此可以二分v来寻找答案。
时间复杂度: log2n
#include
using namespace std;
int n,k;
int cal(int mid){
int tot = 0;
while(mid){
tot+=mid;
mid/=k;
}
return tot;
}
int main(){
while(scanf("%d %d",&n,&k) != EOF){
int up = 1e9+5,down = 1,mid = 0,tot = 0;
int last = mid,ans = mid;
while(down<=up){
mid = down + (up-down)/2; // 防溢出
if(cal(mid) >= n){
ans = mid;
up = mid-1;
}else down = mid + 1;
}
printf("%d\n",ans);
}
return 0;
}
给出一个整数n,当n可以写成 a(a+1)2+b(b+1)2 时 (其中a,b均为整数,ab可以相等),输出YES,否则输出NO。
数据范围: 1≤n≤109
题目并没有要求n的表达唯一,如果可以的话,只需要找出一组解即可。其实和式并没有什么规律。如果采用暴力算法,最坏情况内外层层循环 2n−−√ 次,这时近似有 2n=k2 ,则 k=2n−−√ 。想到这里,其实不难发现,对于特定的i,j其实也满足决策一致性,这样就可以采用二分的方法求解。
时间复杂度: n√logn√
#include
using namespace std;
int main(){
int n;
while(scanf("%d",&n) != EOF){
bool isfind = false;
for(int i = 1;i <= sqrt(2*n);++i){
int fsum = i*(i+1)/2;
int up = sqrt(2*n),down=i;
int ssum = 0,mid = (up+down)/2;
while(down<=up){
mid = (up+down)/2;
ssum = mid*(mid+1)/2;
if(fsum+ssum1;
if(fsum+ssum>n) up = mid-1;
if(fsum+ssum==n){
isfind = true;
break;
}
}
}
if(isfind)printf("YES\n");
else printf("NO\n");
}
return 0;
}
给出一个n×m的格子,格子对应的值位其行列的乘积,求出整张表格中第k个数字。第k个数字的定义是,将整张表的数字按非递减写出,第k个数字即为所求。
数据范围: 1 ≤n, m≤ 5⋅105,1 ≤k ≤ n⋅m
首先读题不要读错,一开始我以为是求第k大的数字,发现样例2说不过去,仔细读了一下题,题目中有他自己的定义。
由于每一行,每一列的数字都是满足单调性的,所以二分一定是一个不错的选择,但是如何进行二分呢?其实可以想到,题目中所说的第k大数字有一个性质:小于第k大的数字的个数,一定小于k个。如对应样例二,1 2 2 3均小于4。如此一来,利用这个单调性,便可以二分枚举k,逐行统计小于k的个数,然后求出答案。
时间复杂度: nlogn
#include
#define ll long long
using namespace std;
ll n,m,k;
ll check(ll mid){
ll sum = 0;
for(int i = 1;i<=n;++i)
sum += min(m,(mid-1)/i);
return sum;
}
int main(){
while(scanf("%lld %lld %lld",&n,&m,&k) !=EOF){
ll up = n*m,down = 1,mid;
ll ans = 0;
while(down<=up){
mid = (up+down)/2;
if(check(mid) >= k) up = mid -1;
else if(tmp < k){
ans = mid;
down = mid+1;
}
}
printf("%lld\n",ans);
}
return 0;
}
给出n个数字,k次操作。每次操作可以将n个数字中的最大数减一,将最小数加一。经过k次操作后,n个数字中的最大值和最小值的差为多少。
数据范围: 1≤n≤500 000,0≤k≤109 , 数列元素 1≤ci≤109
如果暴力算的话,肯定不行。其实想一下,如果进行无限次操作,最大值和最小值会在平均值上下浮动。进行二分的最关键的地方在于决策一致性。有了这个性质,就提供了一个思路。虽然无法直接暴力求出最后的最大值和最小值,但是可以用二分的方法来确定。
首先二分最小值,数列中比最小值小的元素一定是一次次+1变过来的,这样就可以统计出经过了多少次变换。然后跟题目中给出的k做比较,利用决策一致性改变二分上下界,从而求出最小值。
接着二分最大值,数列中比最大值大的元素一定是一次次-1变过来的,同理统计出经过多少次变换。如法炮制可以求出最大值。
最大值和最小值的差即为所求。
这里有一个坑,就是二分初始化上下界。
最小值一定小于等于floor(average),而最大值一定大于等于ceil(average)。如果上下界分别设定为数列中的最大元素和最小元素,这样的话会出现问题:有可能最后求出的最大值比最小值还小。
时间复杂度: nlogn
#include
#define ll long long
using namespace std;
const int nmax = 5e5+5;
ll a[nmax];
ll n,k,minn = 0x3f3f3f3f ,maxn;
ll sum = 0;
bool findmin(int x){
ll tmp = k;
for(int i = 0;i < n;++i){
if(a[i]abs(x-a[i]);
if(tmp<0) return false;
}
return true;
}
bool findmax(int x){
ll tmp = k;
for (int i = 0;i< n ;++i){
if(a[i]>x) tmp -= abs(a[i]-x);
if(tmp<0) return false;
}
return true;
}
int main(){
while(scanf("%lld %lld", &n,&k) != EOF){
sum = 0;
for(int i = 0;i < n;++i){
scanf("%lld", &a[i]);
sum += a[i];
minn = min(minn,a[i]);
maxn = max(maxn,a[i]);
}
ll down = minn,up = sum/n,mid = 0,l,r;
while(down <= up){
mid = down + (up - down) / 2;
if(findmin(mid)){
l = mid;
down = mid + 1;
}else up = mid - 1;
}
down = (ll)ceil(1.0*sum/n),up = maxn ,mid = 0;
while(down <= up){
mid = down + (up - down) / 2;
if(findmax(mid)){
r = mid;
up = mid - 1;
}else down = mid + 1;
}
printf("%lld\n",r-l);
}
return 0;
}
给出n个元素的01序列,现在可以进行k次操作。每次操作可以将0变为1。请你求出操作后最长连续1的序列的长度,并且输出整个序列。
数据范围: 1≤n≤3×105,0≤k≤n
这道题看起来和二分没啥关系。不过想想二分是对枚举的优化,暴力的做法是枚举起点和终点,统计里面0的个数,然后判断个数是否小于k,然后更新答案。对这个枚举思想,其实就可以加以改进。枚举一个起点(或终点)之后,对另外一个端点进行二分判断。如果区间内0的个数小于k,说明可以扩大这个区间,从而更新端点。直至不满足二分上下界为止。
时间复杂度: nlogn
#include
#define nmax 1000000
using namespace std;
int a[nmax];
int sum[nmax];
int n,k;
int cal(int l,int r){
return ((r-l+1) - (sum[r]-sum[l]+1)) + !a[l];
}
int main(){
while(scanf("%d %d",&n,&k)!= EOF){
for(int i = 1;i<=n;++i) scanf("%d",&a[i]);
sum[1] = a[1];
for(int i = 2;i<=n;++i) sum[i] = sum[i-1] + a[i];
int adown = 0,aup = 0,up,down,ans = 0,mid,tmid = 0;
for(int i = 1;i<=n;++i){
down = 1,up = i;
while(down<=up){
mid = (down + up) / 2;
int tmp = cal(mid,i);
if(tmp <= k){
if(i-mid+1>ans){
ans = i-mid+1;
adown = mid;
aup = i;
}
up = mid - 1;
}else if(tmp > k) down = mid + 1;
}
}
bool isfirst = true;
printf("%d\n",ans);
for(int i = 1;i<=n;++i){
if(i>=adown && i<=aup)
if(isfirst) printf("1"),isfirst = false;
else printf(" 1");
else
if(isfirst) printf("%d",a[i]),isfirst = false;
else printf(" %d",a[i]);
}
printf("\n");
}
return 0;
}
给出一个无限长序列,通项为 si=A+(i−1)×B ,现在有n个询问。每个询问包括l,t,m三个参数。每次询问可以执行t次操作,每次操作可以将 [sl,sl+m] 区间内的元素值减一,若 sl 减至为0,l自增1。每次询问中,执行完操作后,全为0的序列部分的右端点是多少。若没有0,输出-1;。
数据范围: 1≤A,B≤106,1≤n≤105,1≤l,t,m≤106
首先可以确定的是,如果 sl<t 那肯定没有0,输出-1即可。
否则的话,二分一个分界点r,这个问题可以变成 sl+sl+1+sl+2+⋯+sl≤t∗m ,所需要求的也就是一个等差数列前n项和。
时间复杂度: nlog(l+t)
#include
#define ll long long
using namespace std;
ll a,b,n,l,t,m;
ll cal(ll x){
return a+(x-1)*b;
}
ll getsum(ll r){
return (cal(l)+cal(r)) * (r-l+1) / 2 ;
}
int main(){
while(scanf("%lld %lld %lld",&a,&b,&n) != EOF){
while(n--){
scanf("%lld %lld %lld",&l,&t,&m);
if(cal(l) > t){
printf("-1\n");
continue;
}else{
ll up = (t-a)/b + 1,down = l,mid,ans;
while(down <= up){
mid = (up+down) / 2;
if(getsum(mid)<=t*m){
ans = mid;
down = mid+1;
}else up = mid-1;
}
printf("%lld\n",ans);
}
}
}
return 0;
}
未完待续……