二分以及练习题目

二分模板

判断是否可以二分

(1)单调性

备选答案集是有序的

(2)二段性

在检查了mid是否符合要求之和,我可以舍弃左右某一边的答案

两个模板

  1. 关键词:满足条件的最小值,最大值最小,某个有序区间内某个数>=或者>key的第一个元素,或者当正确答案在左边时

    while(l < r) {
     //check函数求当前值为mid情况下是否合法,如果合法,会想更小一点的值是不是合法的,所以会舍弃掉右边,去左边寻找更小的值,即r向左移动,因为mid是合法的,不能不要它,所以r=mid
    		if(check(mid)) {//满足要求,要求最小,再向左找看有没有更小的也满足要求
    			r = mid;//因为mid也满足要求,所以要保留
     //如果不合法,只能扩大当前值,所以会舍弃掉左边,去右边寻找更大的值,即l向右移动,因为mid是不合法的,直接不要它,所以l=mid+1
    		}else{
    			l = mid + 1;//mid不满足要求,直接跳过就行
    		}
    	}
    

    假设出现如下情况,l=l,r=l+1,此时mid=l。如果mid = (l+r)/2

    (1)check(mid)返回为真,r = mid = l,正常退出

    (2)check(mid)返回为假,l = r,正常退出

  2. 关键词:满足条件的最大值,最小值最大,某个有序区间内某个数<=或者<key的最后一个元素,或者当正确答案在左边时

while(l < r) {
		long mid = (l+r+1)/2;//注意这里要+1
		if(check(mid)) {//满足要求,要求最大,再向右找看有没有更大的也满足要求
			l = mid;//因为mid也满足要求,所以要保留
		}else{
			r = mid - 1;//mid不满足要求,直接跳过就行
		}
	}

假设出现如下情况,l=l,r=l+1,此时mid=l。如果mid = (l+r)/2

(1)check(mid)返回为真,l = mid = l,那么又出现了最开始的状态即l=l,r=l+1,此时会陷入无限循环

(2)check(mid)返回为假,r < l,正常退出

假设出现如下情况,l=l,r=l+1,此时mid=l+1。如果mid = (l+r+1)/2

(1)check(mid)返回为真,l = mid = l+1=r,正常退出

(2)check(mid)返回为假,r = l,正常退出

分巧克力

题目分析

第一阶段二段性分析

希望巧克力的边长最大,而巧克力的边长要满足一个条件,就是按照该边长切出来的巧克力的块数应该大于等于K块才能满足K位小朋友都有巧克力。那么现在就是满足条件的最大值,我们要看一下他是否符合二段性,二分的关键在于二段性。

对于边长为mid的巧克力,如果它可以切出k块巧克力,那么我们可以确定边长小于mid的巧克力一定也可以,但是此时我需要找的是最大的边长,那么mid一定比小于mid的值更大,所以小于mid的值我就不用管了,也就是我可以确定我能够舍弃掉mid左边的值。我还想要确定比mid更大的边长是否也满足条件,所以我要在mid的右边继续二分。

if (check(mid)) {l = mid;} //因为mid是符合条件的,所以我要留着它,而不是l=mid+1

对于边长为mid的巧克力,如果它不可以切出k块巧克力,那么我们可以确定边长大于mid的巧克力一定也不可以,所以大于等于mid的值我就不用管了,也就是我可以确定我能够舍弃掉mid右边的值。我还想要寻找比mid更小的边长是否能满足条件,所以我要在mid的左边继续二分。

else { r = mid - 1; }//因为mid是不符合条件的,所以我不要留着它,而不是r=mid
//主要这里出现了减法,那么求mid那么应该是(l+r+1)/2

综上该题满足二段性,可以用二分,二分的板子就不说了,接下来说一下check函数如何写。

第二阶段写check函数

check(u)要实现的作用是检查边长为u的情况下能否切出k块巧克力。已知某块巧克力为 W ∗ H W*H WH大小,那么能够切出来的巧克力个数用 ( W / u ) ∗ ( H / u ) (W/u)*(H/u) (W/u)(H/u)表示。注意不能用 ( W ∗ H ) / ( u ∗ u ) (W*H)/(u*u) (WH)/(uu)表示。那么只需要遍历当前每一块巧克力,求出能个切割的巧克力总数和k比较就可以了,代码如下,

private static boolean check(int[][] a, int mid, int k) {
    int sum = 0;
    for (int i = 0; i < a.length; i++) {
        sum =sum + (a[i][0]/mid)*(a[i][1]/mid);
    }
    if(sum >= k) {
        return true;
    }
    return false;
}

第三步二分范围确定

那么这里的边长的最小值是1,最大值就是巧克力的最大边长,也就是100000。

题目代码

import java.util.Scanner;
public class Main {
public static void main(String[] args) {
    Scanner scanner = new Scanner(System.in);
    int n = scanner.nextInt();
    int k = scanner.nextInt();
    int a[][] = new int[n][2];
    for (int i = 0; i < n; i++) {
        a[i][0] = scanner.nextInt();
        a[i][1] = scanner.nextInt();
    }
    int l = 1;
    int r = 100000;
    while(l<r) {
        int mid = (l + r + 1) / 2;
        if(check(a,mid,k)) {
            l = mid;
        }else {
            r = mid - 1;
        }
    }
    System.out.println(l);
}

private static boolean check(int[][] a, int mid, int k) {
    int sum = 0;
    for (int i = 0; i < a.length; i++) {
        sum =sum + (a[i][0]/mid)*(a[i][1]/mid);
    }
    if(sum >= k) {
        return true;
    }
    return false;
}
}
#include <bits/stdc++.h>
using namespace std;
const int N=100010;
int n,k,u;
int h[N],w[N];
//check函数 检查是否能切k块巧克力
int m;//巧克力块数
bool check(int mid){
    m=0;
	for(int i=0;i<n;i++){
		m+=(w[i]/mid)*(h[i]/mid);
	}
	if(m>=k) return true;
    else return false;
}
int main(){
	cin>>n>>k;
	int l,r;
	 l=1;r=1e5;
	for(int i=0;i<n;i++){
		cin>>h[i]>>w[i];
	}
	int mid;
	while(l<r){
		mid=(l+r+1)/2;
		if(check(mid)) l=mid;
		else r=mid-1;
		}
	cout<<l;
	return 0;
}
n,k=map(int,input().split())
a=[]
for i in range(n):
  x,y=map(int,input().split())
  a.append((x,y))
def check(m):
  n=0
  for x,y in a:
     n +=(x//m)*(y//m)
  return n>=k
left,right=1,100000
ans=1
while left<right:
  mid=(left+right+1)//2
  if check(mid):
    left=mid
  else:
    right=mid-1
print(left)

求阶乘

题目分析

第一阶段二分板子分析

我们要求满足N!末尾0的个数恰好为K的最小的N是不是就是求满足条件的最小,还是要考虑一下二分。

如果对于mid!末尾0的个数比K少,那么所有比mid小的值的阶乘包含的5的个数肯定更少,所以我可以把mid左边的值都舍弃掉,继续向mid右边寻找。

if(f(mid) < k ) {  l = mid + 1; }//此时mid是不符合条件的,我要舍弃它,所以l=mid+1,而不是mid

如果对于mid!末尾0的个数比K大,那么所有比mid大的值的阶乘包含的5的个数肯定更多,所以我可以把mid右边的值都舍弃掉,继续向mid左边寻找。

else { r = mid; }//此时mid是符合条件的,我要留着它,所以r=mid,而不是mid-1

综上满足二分的二段性,而check函数就是求解N!里面5的个数,接下来讲解。

第二阶段写check函数

如果直接去求,我们看一下,K的范围已经到了 1 18 1^{18} 118,这么大肯定有什么巧妙的地方,而不是直接求解。对于一个数字N而言,我们怎么去求他的阶乘末尾包含0的个数?末尾的0是如何产生的?是不是由 2 ∗ 5 = 10 2*5=10 25=10产生的?我们只需要求N!里面包含几个2或者几个5就行了,那么由2或者5个数的最小值决定末尾0的个数,那么2和5而言,比如1-20里面,5,10,15,20包含5,而2,6,8,10,12,14,16,20里面包含2,也就是5的个数必然比2少,那么只需要求N!里面包含5的个数就可以了。那么这个求法大家可以记住,代码如下,

private static long f(long x) {
    long res = 0;
    while(x > 0) {
        res += x / 5;
        x = x / 5;
    }
    return res;
}

求x!里面包含5的个数,令x/5,直到x的值为0就停止。比如25里面5的个数等于sum+=25/5=sum+=5。25/5=5不等于0,所以还要再来一次sum+=5/5=sum+=1。此时就可以了。那么25里面包含6个5。其中5,10,15,20,都包含1个5,而25里面两个5,所以一共6个5。

第三阶段二分范围确定

这一道题,l=0,但是r应该如何确定呢?第一种方法直接一点,k=1e18,明显是long的范围,那么r我就初始化为long的最大值Long.MAX_VALUE是完全没有问题的。第二种方法,你要去求了,求一个精确的值,那么怎么求呢?我要求谁的阶乘末尾0的个数是1e18,你可以一个个试,

我试的过程如下图,最终得出了5e18就可以。

check函数是第二步介绍的,来求某个数的阶乘后面0的个数。

但是注意这里,我不能确定一定可以找到一个满足条件的解,对于分巧克力那道题,题目说了每个小朋友能够至少获得一块边长为1的巧克力,所以边长为1一定满足条件,一定有解。而本题也说了,这样的N可能不存在,所以在二分结束后,我要确定一下,当前的值是否满足条件。

题目代码

import java.util.Scanner;
public class Main {
public static void main(String[] args) {
    Scanner scanner = new Scanner(System.in);
    long k = scanner.nextLong();
    long l = 1,r = Long.MAX_VALUE-10;
    while(l < r) {
        long mid = (l+r)/2;
        if(f(mid) < k ) {
            l = mid + 1;
        }else {
            r = mid;
        }
    }
    if(f(l) == k) {
        System.out.println(l);
    }else {
        System.out.println(-1);
    }
   
}

private static long f(long x) {
    long res = 0;
    while(x > 0) {
        res += x / 5;
        x = x / 5;
    }
    return res;
}
}

卡牌

题目分析

想一下前面题的特点,是不是都出现了“最大边长”,“最小的数”这种字眼,那么这里出现了“最多能凑出多少套牌”,我们可以考虑用二分。接下来我们要看一下他是否符合二段性,二分的关键在于二段性。

第一阶段二段性分析

对于mid套牌,如果我们可以凑出来,那么我们可以确定套数小于mid的牌一定也可以,但是此时我需要找的是最多,那么mid一定比小于mid的值更大,所以小于mid的值我就不用管了,也就是我可以确定我能够舍弃掉mid左边的值。我还想要确定比mid更大的值是否也满足条件,所以我要在mid的右边继续二分。

if (check(mid)) {l = mid;} //因为mid是符合条件的,所以我要留着它,而不是l=mid+1

对于mid套牌,如果我们不可以凑出来,那么我们可以确定大于mid的套数一定也不可以,所以大于等于mid的值我就不用管了,也就是我可以确定我能够舍弃掉mid右边的值。我还想要寻找比mid更小的值是否能满足条件,所以我要在mid的左边继续二分。

else { r = mid - 1; }//因为mid是不符合条件的,所以我不要留着它,而不是r=mid
//主要这里出现了减法,那么求mid那么应该是(l+r+1)/2

综上该题满足二段性,可以用二分,二分的板子就不说了,接下来说一下check函数如何写。

第二阶段写check函数

check(u)要实现的作用是检查我能否凑出u套牌,也就是n种牌每一种的张数至少是n。我现在有m张空牌可以用,但是也有一个限制对于第i种牌,我手写的个数不能超过b[i]。整体思路是如果a[i]<u,我要手写u-a[i]张牌,如果u-a[i]>b[i],然后我还要记录目前我手写的总牌数,超过m也是返回false。我可以返回false具体请看代码,

public static boolean check(int num) {//num为可以凑出来的卡牌组数
        long temp = m;//备份空白牌的数量
        for (int i = 0; i < a.length; i++) {//遍历卡牌
            if (a[i] >= num) continue;
            //如果卡牌数现在不满足至少为num张
            int add = num - a[i];//需要添加的扑克牌数量
            temp -= add;
            if (temp < 0) return false;//如果剩余的牌不够
            if (add > b[i]) return false;//如果超过预计需要画的牌个数
        }
        return true;
    }

第三阶段二分范围确定

l的值好确定,就是0,那么r呢?先来看一下a[i]的最大值是4e5,也就是每种牌我最多有4e5张,b[i]也是最多可以手写4e5张,那么加起来就是8e5,因此r可以取8e5+5。后面的5是随便加的数,一般要比你算出来的大一些就可以。

题目代码

import java.util.Scanner;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Arrays;
public class Main {
    static int n;
    static long m;
    static int[] a, b;
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        String line = br.readLine();
        n = Integer.parseInt(line.split(" ")[0]);
        m = Long.parseLong(line.split(" ")[1]);//空白牌的数量
        a = new int[n];
        b = new int[n];
        int l = 0, r = 800005;
        String[] s = br.readLine().split(" ");
        for (int i = 0; i < n; i++) {
            a[i] = Integer.parseInt(s[i]);
        }
        s = br.readLine().split(" ");
        for (int i = 0; i < n; i++) {
            b[i] = Integer.parseInt(s[i]);
        }
        while (l < r) {//获取最大值
            int mid = (l + r + 1) / 2;
            if (check(mid)) {
                l = mid;
            } else {
                r = mid - 1;
            }
        }
        System.out.println(l);
    }
    public static boolean check(int num) {//num为可以凑出来的卡牌组数
        long temp = m;//备份空白牌的数量
        for (int i = 0; i < a.length; i++) {//遍历卡牌
            if (a[i] >= num) continue;
            //如果卡牌数现在不满足
            int add = num - a[i];//需要添加的扑克牌数量
            temp -= add;
            if (temp < 0) return false;//如果剩余的牌不够
            if (add > b[i]) return false;//如果超过预计需要画的牌个数
        }
        return true;
    }
}
#include <stdio.h>
#include <stdbool.h>
#define N 1000010

int n;
long long m;
int a[N], b[N];

bool check(int x){
	long long s = 0;
	for (int i = 0; i < n; i ++){
		if (a[i] >= x)
			continue;
		if (x - a[i] <= b[i])
			s += x - a[i];
		else return false;
	}
	if (s <= m)
		return true;
	return false;
}
int main(){
	scanf("%d%lld", &n, &m);
	for (int i = 0; i < n; i ++){
		scanf("%d", &a[i]);
	}

	for (int i = 0; i < n; i ++){
		scanf("%d", &b[i]);
	}
	int l = 0;
	int r = 8e8;
	while (l < r){
		int mid = (l + r + 1) / 2;
		if (check(mid)){
			l = mid;
		}
		else
			r = mid - 1;
	}
	printf("%d",l);
	return 0;
}

美丽的区间

题目分析

读题发现了一个关键字,“区间长度最短”,考虑用二分。对于一个长度为mid的区间,如果它的左右端点都不固定,其实我们不知道这个区间是哪个,因为长度为2的区间,可以是区间[1,2]也可以是区间[2,3]。所以我们固定区间端点,那么此时我们枚举左边界i,右边界二分去找,那么此时的mid表示的是区间右边界。如果mid往右走变大,区间长度会增大,如果mid往左走变小,区间长度会变小。区间长度为mid-i+1。

第一阶段二段性分析

对于长度为mid-i+1的区间,如果我们可以找到它的区间和大于等于S,那么我们可以确定右边界大于mid的区间一定也可以,因为区间里的值都是正数。但是此时我需要找的是最短,那么mid一定比大于mid的值更小,所以大于mid的值我就不用管了,也就是我可以确定我能够舍弃掉mid右边的值。我还想要确定比mid更小的值是否也满足条件,所以我要在mid的左边继续二分。

if(sum[mid] - sum[i-1] >= s) { r = mid;}//因为mid是符合条件的,所以我要留着它,而不是r=mid-1

对于长度为mid-i+1的区间,如果我们不可以找到它的区间和大于等于S,那么我们可以确定右边界小于mid的区间一定也不可以,因为区间里的值都是正数。所以小于mid的值我就不用管了,也就是我可以确定我能够舍弃掉mid左边的值。我还想要找比mid更大的值是否可以满足条件,所以我要在mid的右边继续二分。

else {l = mid + 1;}//因为mid是不符合条件的,所以我不要留着它,而不是l=mid

综上该题满足二段性,可以用二分,二分的板子就不说了,接下来说一下check函数如何写。

第二阶段写check函数

check(u)要实现的作用是检查区间和是否大于等于S,而求区间和最快的方式是利用前缀和求。

for (int i = 1; i < a.length; i++) {
        a[i] = scanner.nextInt();
        sum[i] = sum[i-1] + a[i];
    }
.......
if(sum[mid] - sum[i-1] >= s) {
       r = mid;
}else {
       l = mid + 1;
}

第三阶段二分范围确定

mid表示的是右边界,当左边界为i时,右边节最小去i,最大取n。

每次二分结束,我都要确定一下当前得到的l是否满足条件。最后输出的时候还是要确定一下我是否找到了满足条件的区间,因为这样的区间可能不存在。
mid表示的是右边界,当左边界为i时,右边节最小去i,最大取n。

解释一下下面的代码,

if(sum[r] - sum[i-1] >= s) {
            ans = Math.min(ans, r-i+1);    
    }

为什么会有它呢?我们要知道二分求出来的l是什么,我们求出来的l是在区间左端点固定为i的情况下的一个答案,我们有多个左端点,也意味着我们有多个答案,那么我们求最短区间,就在这多个答案里求一个最小值就可以了,那么如何来表示答案呢。还是要注意我们二分求出来的l是左端点为i的情况下的右端点,那么我们的答案是区间长度,怎么表示呢?就是r-i+1,就是区间[i,l]的长度,因为这里是二分,l和r其实是相等的,写成r刚好和右端点对应,所以这里是r-i+1。

题目代码

import java.util.Scanner;
public class Main {
public static void main(String[] args) {
    Scanner scanner = new Scanner(System.in);
    int n = scanner.nextInt();
    long s = scanner.nextLong();
    int a[] = new int[n+1];
    long sum[] = new long[n+1];
    for (int i = 1; i < a.length; i++) {
        a[i] = scanner.nextInt();
        sum[i] = sum[i-1] + a[i];
    }
    long ans = Integer.MAX_VALUE;
    for (int i = 1; i < n + 1; i++) {//枚举右边界
        int l = i,r = n;
        while(l < r) {
            int mid = (l + r)/2;
            if(sum[mid] - sum[i-1] >= s) {
                r = mid;
            }else {
                l = mid + 1;
            }
        }
        if(sum[r] - sum[i-1] >= s) {
            ans = Math.min(ans, r-i+1);    
        }
    }
    if(ans == Integer.MAX_VALUE) {
        System.out.println(0);
    }else {
        System.out.println(ans);
    }
}
}

123

二分+等差数列求和+前缀和数组

题目分析

连续一段的和我们想到了前缀和,但是这里的l和r的范围为1e12,明显不能用O(n)的时间复杂度去求前缀和。那么我们开始观察序列的特点,可以按照等差数列对序列进行分块。如上图,在求前10个数的和的时候,我们可以这样求1+3(1+2)+6(1+2+3)+10(1+2+3+4)=20。我们不需要遍历10次就可以求出来。求前缀和代码如下

sum = new long[1500010];
for (int i = 1; i <= 1500000; i++) {
    t += i;
    sum[i] = sum[i-1]+t;
}

这里的t最开始等于1,是第一块的数字和,然后等于3,是第二块数字的和,然后等于6是第三块数字的和,以此类推。sum[i]表示的是分块后前i块包含数字的和。

我们可以求出前n块数字的和,那么我们需要知道第l个数字是在哪一块里面,然后求第l个数字是所在块的第几个数字。因为对于最后一块来说,不是所有的数字都会被包含进来,我们要单独对最后一块求数字和。

第一阶段利用二分求第x个数所在的块

​ 图1

如图1所示,我们可以把这个序列分块,第一块有1个数,第二块有2个数,第三块有3个数,第四块有4个数,每一块拥有数的个数是一个等差数列。现在要找到序列中第x个数所在的块数,假设x=3,那么第x个数在第2块中,如果x=4,那么第x个数在第3块中。求第x个数所在的块数,就是求从左往右数,前缀和第一个大于等于x的块。

比如第2块的前缀和就是3,第三块的前缀和就是5。这个可以用二分去求。

    int l = 1;
    int r = 1500000;//最多可以分的块数
    while(l < r) {
        int mid = (l + r) / 2;
        if(sum(mid) < x) {//求mid个块中包含的数字的个数,如果小于x,就是不符合条件,我要向左找
            l = mid + 1;
        }else {//符合条件,我要看前面的块是否也是大于等于x的
            r = mid;
        }
    }

这里的sum函数的作用就是求前mid块中包含的数字的个数,因为是等差数列,所以直接用等差数列的求和公式就可以了,sum函数如下

private static long sum(long x) {    
    return (1L + x) * x / 2;
}

第二阶段求前x个数的前缀和

接下来分析二分结束之和我要怎么操作,我要求前x个数字的和。

假设x=8,那么第x个数在第4块中,我还要知道,第x个数是第4块中的第几个数字。如图,第4块有4个数,第x个数第4块的第2个数上,那么第2个数是怎么来的呢?就是x-sum(r-1)=8-6=2。其实就是我二分算出来了第x个数在第r块上,那么我只需要把前r-1块包含的数的个数减去就算出来x是在第r块上的第几个数上了。结合上图更好理解。那么前x个数的和就是前r-1块包含数的和加上第r块里面前x-sum(r-1)个数的和。

某一块里面包含的数也是等差数列,求前n个数的和依然可以直接用sum(n)去求。而数组sum[r]里面记录的是前r块包含数字值的前缀和。所以二分结束后的代码是这样子的

private static long f(long x) {
    int l = 1;
    int r = 1500000;//最多可以分的块数
    while(l < r) {
        int mid = (l + r) / 2;
        if(sum(mid) < x) {//求mid个块中包含的数字的个数
            l = mid + 1;
        }else {
            r = mid;
        }
    }
    //r--是表示完整的块的个数
    r--;//就是上文里的r-1
    //x表示x处在他所在块的第几个位置,需要减去前面所有块包含的数的个数
    //本来要求x个数字,前r个块中已经包含了sum(r)个数字,第r+1个块
    x -= sum(r);//前r个块中已经包含了多少个数字
    return sum[r]+sum(x);
}

还是对于x=8的例子,二分求出来r=4,r–后,r=3,sum[3]=10。x-=sum(3)=8-6=2。sum[3]+sum(2)=10+3=13

注意这道题里对于sum函数的多次应用,但是不是每一次应用含义都是一样的。因为每一块包含的数字个数是等差数列,而每一块内每个数字形成的也是等差数列。

题目代码

import java.util.Scanner;
public class Main {
static long[] sum;
public static void main(String[] args) {
    Scanner scanner = new Scanner(System.in);
    long t = 0;
    //前缀和的预处理
    sum = new long[1500010];
    for (int i = 1; i <= 1500000; i++) {
        t += i;
        sum[i] = sum[i-1]+t;
    }
    int n = scanner.nextInt();
    while(n-- > 0) {
        long l = scanner.nextLong();
        long r = scanner.nextLong();
        System.out.println(f(r)-f(l-1));//f(r)求的是序列中前r个数的和
    }
}
//二分  二分求的是x在哪一块中 n*(n-1)/2>l的第一个n
private static long f(long x) {
    int l = 1;
    int r = 1500000;//最多可以分的块数
    while(l < r) {
        int mid = (l + r) / 2;
        if(sum(mid) < x) {//求mid个块中包含的数字的个数
            l = mid + 1;
        }else {
            r = mid;
        }
    }
    //r--是表示完整的块的个数
    r--;
    //x表示x处在他所在块的第几个位置,需要减去前面所有块包含的数的个数
    //本来要求x个数字,前r个块中已经包含了sum(r)个数字,第r+1个块
    x -= sum(r);//前r个块中已经包含了多少个数字
    return sum[r]+sum(x);
}
//sum函数求前x块包含的数的个数,同时也可以表示某一个块中,前x个数的总和
private static long sum(long x) {
    // TODO Auto-generated method stub    
    return (1L + x) * x / 2;
}
}

晾衣服

题目分析

这里出现了“最小化干燥的总时间”,那么可以考虑用二分去做。

第一阶段二段性分析

假设当前需要耗费的时间为mid分钟,如果mid分钟内可以烘干这些衣服,那么我们可以确定右边界大于mid的区间一定也可以。但是此时我需要找的是最短时间,那么mid一定比大于mid的值更小,所以大于mid的值我就不用管了,也就是我可以确定我能够舍弃掉mid右边的值。我还想要确定比mid更小的值是否也满足条件,所以我要在mid的左边继续二分。

if(check(mid)) {r = mid;}//因为mid是符合条件的,所以我要留着它,而不是r=mid-1

假设当前需要耗费的时间为mid分钟,如果mid分钟内不可以烘干这些衣服,那么我们可以确定右边界小于mid的区间一定也不可以。所以小于mid的值我就不用管了,也就是我可以确定我能够舍弃掉mid左边的值。我还想要找比mid更大的值是否可以满足条件,所以我要在mid的右边继续二分。

else {l = mid + 1;}//因为mid是不符合条件的,所以我不要留着它,而不是l=mid

综上该题满足二段性,可以用二分,二分的板子就不说了,接下来说一下check函数如何写。

第二阶段写check函数

check(u)要实现的作用是检查能否在mid分钟内烘干这些衣服。对于一个衣服的湿度a[i],如果他大于mid,就需要使用烘干机,使用的时间是(a[i]-mid)/(k-1)。因为烘干衣服不足1分钟也要按一分钟算,所以这里要上取整。

这里为什么是k-1呢?我们要保证在mid分钟内烘干,因为衣服每一分钟湿度都会减1,如果我把衣服放在烘干机上湿度会减k,那么放在烘干机和不放在烘干机上会比原来多减了k-1。我们要注意的是,烘干机使用的总时间不能超过mid。

假设我只有一个衣服需要烘干,如果mid=3,a[i]=10,k=3。a[i]-mid=10-3=7。如果是7/3=3。这样看mid等于3是可以的,但是我把烘干机全用在这一个衣服上所需要的烘干时间是10/3=4>mid=3,明确不可以在3分钟内烘干该衣服。所以我这里应该是7/2=4,此时可以判断出来不可以在3分钟内烘干该衣服。check函数如下,

static boolean check(int mid) {
        long s = 0;
        for (int i = 0; i < n; i++) {
            if (a[i] > mid) {
                s += (long)(a[i] - mid + k - 2) / (k - 1);
            }
        }
        return s <= mid;
    }

第三阶段二分范围确定

烘干的时间最长就是不使用烘干机,自然风干需要a[i]分钟,而a[i]最大是1e9,所以l=0,r=1e9。

注意一个特殊情况,如果k=1,那么其实烘干机有和没有都一样,自然风干所需要的时间就是所有衣服中最大的湿度。

for (int i = 0; i < n; i++) {
        a[i] = sc.nextInt();
        maxV = Math.max(maxV, a[i]);
}
k = sc.nextInt();
if (k == 1) {
    System.out.println(maxV);
    return;
}

题目代码

import java.util.Scanner;
public class Main {
    static int N = 100010;
    static int n, k;
    static int[] a = new int[N];
    public static void main(String[] args) {
    	Scanner sc = new Scanner(System.in);
        n = sc.nextInt();
        int maxV = 0;
        for (int i = 0; i < n; i++) {
            a[i] = sc.nextInt();
            maxV = Math.max(maxV, a[i]);
        }
        k = sc.nextInt();
        if (k == 1) {
            System.out.println(maxV);
            return;
        }
        int l = 1, r = (int) 1e9;
        int res = 0;
        while (l < r) {
            int mid = (l + r)/2;
            if (check(mid)) {
                r = mid;
            } else {
                l = mid + 1;
            }
        }
        System.out.println(l);
    }

    static boolean check(int mid) {
        long s = 0;
        for (int i = 0; i < n; i++) {
            if (a[i] > mid) {
                s += Math.ceil((a[i]-mid)*1.0 / (k - 1));
            }
        }
        return s <= mid;
    }
}

妮妮的月饼工厂

题目分析

这里出现了“最高高度”,那么可以考虑用二分去做。

第一阶段二段性分析

希望月饼的高度最大,而月饼的高度要满足一个条件,就是按照该高度切出来的月饼的块数应该大于等于K块。那么现在就是满足条件的最大值,我们要看一下他是否符合二段性,二分的关键在于二段性。

对于高度为mid的月饼,如果它可以切出k块月饼,那么我们可以确定高度小于mid的月饼一定也可以,但是此时我需要找的是最大的高度,那么mid一定比小于mid的值更大,所以小于mid的值我就不用管了,也就是我可以确定我能够舍弃掉mid左边的值。我还想要确定比mid更大的高度是否也满足条件,所以我要在mid的右边继续二分。

if (check(mid)) {l = mid;} //因为mid是符合条件的,所以我要留着它,而不是l=mid+1

对于高度为mid的月饼,如果它不可以切出k块月饼,那么我们可以确定边长大于mid的月饼一定也不可以,所以大于等于mid的值我就不用管了,也就是我可以确定我能够舍弃掉mid右边的值。我还想要寻找比mid更小的月饼是否能满足条件,所以我要在mid的左边继续二分。

else { r = mid - 1; }//因为mid是不符合条件的,所以我不要留着它,而不是r=mid
//主要这里出现了减法,那么求mid那么应该是(l+r+1)/2

综上该题满足二段性,可以用二分,二分的板子就不说了,接下来说一下check函数如何写。

第二阶段写check函数

check(u)要实现的作用是检查高度为u的情况下能否切出k块月饼。已知某个月饼高度为 H H H大小,那么能够切出来的月饼个数用 ( H / u ) (H/u) (H/u)表示。那么只需要遍历当前每一块月饼,求出能个切割的月饼总数和k比较就可以了,代码如下,

private static boolean check(int u) {
	long res = 0l;
	for(int i = 1;i <=n;i++) {
		res += a[i]/u;
	}
	if(res >= k) return true;
	return false;
}

第三步二分范围确定

那么这里的高度的最小值是1,最大值就是月饼的最大边长,也就是1e9。注意本题没有说一定能找到符合条件的高度,所以最后输出的时候要判断一下。

题目代码

import java.util.Scanner;
public class Main {
	static long a[];
	static int n;
	static long k;
public static void main(String[] args) {
	Scanner scanner = new Scanner(System.in);
	n = scanner.nextInt();
	k = scanner.nextLong();
	a = new long[n+1];
	for(int i = 1;i<=n;i++) {
		a[i]=scanner.nextInt();
	}
	int l = 1,r = (int) 1e9;
	while(l < r) {
		int mid = (l+r+1)/2;
		if(check(mid)) l = mid;
		else r = mid-1;
	}
	if(!check(l)) System.out.println(-1);//忘了这个判断
	else System.out.println(l);
}
private static boolean check(int u) {
	// TODO Auto-generated method stub
	long res = 0l;
	for(int i = 1;i <=n;i++) {
		res += a[i]/u;
	}
	if(res >= k) return true;
	return false;
}
}
  • 32
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
微机课后题答案啊 微机接口技术练习题解 第1章 绪论 1. 计算机分那几类?各有什么特点? 答:传统上分为三类:大型主机、小型机、微型机。大型主机一般为高性能的并行处理系统, 存储容量大,事物处理能力强,可为众多用户提供服务。小型机具有一定的数据处理能力,提供一定用户规模的信息服务,作为部门的信息服务中心。微型机一般指在办公室或家庭的桌面或可移动的计算系统,体积小、价格低、具有工业化标准体系结构,兼容性好。 2. 简述微处理器、微计算机及微计算机系统三个术语的内涵。 答:微处理器是微计算机系统的核心硬件部件,对系统的性能起决定性的影响。微计算机包括微处理器、存储器、I/O接口电路及系统总线。微计算机系统是在微计算机的基础上配上相应的外部设备和各种软件,形成一个完整的、独立的信息处理系统。 3. 80X86微处理器有几代?各代的名称是什么? 答:从体系结构上可分为3代: ◆ 8080/8085:8位机。 ◆ 8086/8088/80286:16位机。 ◆ 80386/80486:32位机。 第2章 微处理器结构及微计算机的组成 1. 8086是多少位的微处理器?为什么? 答:8086是16位的微处理器,其内部数据通路为16位,对外的数据总线也是16位。 2. EU与BIU各自的功能是什么?如何协同工作? 答:EU是执行部件,主要的功能是执行指令。BIU是总线接口部件,与片外存储器及I/O接口电路传输数据。EU经过BIU进行片外操作数的访问,BIU为EU提供将要执行的指令。EU与BIU可分别独立工作,当EU不需BIU提供服务时,BIU可进行填充指令队列的操作。 3. 8086/8088与其前一代微处理器8085相比,内部操作有什么改进? 答:8085为8位机,在执行指令过程中,取指令与执行执令都是串行的。8086/8088由于内部有EU和BIU两个功能部件,可重叠操作,提高了处理器的性能。 4. 8086/8088微处理器内部有那些寄存器,它们的主要作用是什么? 答:执行部件有8个16位寄存器,AX、BX、CX、DX、SP、BP、DI、SI。AX、BX、CX、DX一般作为通用数据寄存器。SP为堆栈指针存器,BP、DI、SI在间接寻址时作为地址寄存器或变址寄存器。总线接口部件设有段寄存器CS、DS、SS、ES和指令指针寄存器IP。段寄存器存放段地址,与偏移地址共同形成存储器的物理地址。IP的内容为下一条将要执行指令的偏移地址,与CS共同形成下一条指令的物理地址。 5. 8086对存储器的管理为什么采用分段的办法? 答:8086是一个16位的结构,采用分段管理办法可形成超过16位的存储器物理地址,扩大对存储器的寻址范围 (1MB,20位地址)。若不用分段方法,16位地址只能寻址64KB空间。 6.在8086中,逻辑地址、偏移地址、物理地址分别指的是什么?具体说明。 答:逻辑地址是在程序中对存储器地址的一种表示方法,由段地址和段内偏移地址两部分组成,如1234H:0088H。偏移地址是指段内某个存储单元相对该段首地址的差值,是一个16位的二进制代码。物理地址是8086芯片引线送出的20位地址码,用来指出一个特定的存储单元。 7.给定一个存放数据的内存单元的偏移地址是20C0H,(DS)=0C00EH,求出该内存单元的物理地址。 答:物理地址:320F8H。 8.8086/8088为什么采用地址/数据引线复用技术? 答:考虑到芯片成本,8086/8088采用40条引线的封装结构。40条引线引出8086/8088的所有信号是不够用的,采用地址/数据线复用引线方法可以解决这一矛盾,从逻辑角度,地址与数据信号不会同时出现,二者可以分时复用同一组引线。 9. 8086与8088的主要区别是什么? 答:8086有16条数据信号引线,8088只有8条;8086片内指令预取缓冲器深度为6字节,8088只有4字节。 10. 怎样确定8086的最大或最小工作模式?最大、最小模式产生控制信号的方法有何不同 答:引线MN/MX#的逻辑状态决定8086的工作模式,MN/MX#引线接高电平,8086被设定为最小模式,MN/MX#引线接低电平,8086被设定为最大模式。 最小模式下的控制信号由相关引线直接提供;最大模式下控制信号由8288专用芯片译码后提供,8288的输入为8086的S2#~S0#三条状态信号引线提供。 11. 8086被复位以后,有关寄存器的状态是什么?微处理器从何处开始执行程序? 答:标志寄存器、IP、DS、SS、ES和指令队列置0,CS置全1。处理器从FFFFOH存储单元取指令并开始执行。 12.8086基本总线周期是如何组成的?各状态中完成什么基本操作? 答:基本总线周期由4个时钟(CLK)周期组成,按时间顺序定义为T1、T2、T3、T4。在T1期间8086发出访问的地的地址信号和地址锁存选通信号ALE;T2期间发出读写命令信号RD#、WR#及其它相关信号;T3期间完成数据的访问;T4结束该总线周期。 13.结合8086最小模式下总线操作时序图,说明ALE、M/IO#、DT/R#、RD#、READY信号的功能。 答:ALE为外部地址锁存器的选通脉冲,在T1期间输出;M/IO#确定总线操作的对象是存储器还是I/O接口电路,在T1输出;DT/R#为数据总线缓冲器的方向控制信号,在T1输出;RD#为读命令信号;在T2输出;READY信号为存储器或I/O接口“准备好”信号,在T3期间给出,否则8086要在T3与T4间插入Tw等待状态。 14.8086中断分哪两类?8086可处理多少种中断? 答:8086中断可分为硬件中断和软件中断两类。8086可处理256种类型的中断。 15.8086可屏蔽中断请求输入线是什么?“可屏蔽”的涵义是什么? 答:可屏蔽中断请求输入线为INTR;“可屏蔽”是指该中断请求可经软件清除标志寄存器中IF位而被禁止。 16.8086的中断向量表如何组成?作用是什么? 答:把内存0段中0~3FFH区域作为中断向量表的专用存储区。该区域存放256种中断的处理程序的入口地址,每个入口地址占用4个存储单元,分别存放入口的段地址与偏移地址。 17.8086如何响应一个可屏蔽中断请求?简述响应过程。 答:当8086收到INTR的高电平信号时,在当前指令执行完且IF=1的条件下,8086在两个总线周期中分别发出INTA#有效信号;在第二个INTA#期间,8086收到中断源发来的一字节中断类型码;8086完成保护现场的操作,CS、IP内容进入堆栈,请除IF、TF;8086将类型码乘4后得到中断向量表的入口地址,从此地址开始读取4字节的中断处理程序的入口地址,8086从此地址开始执行程序,完成了INTR中断请求的响应过程。 18.什么是总线请求?8086在最小工作模式下,有关总线请求的信号引脚是什么? 答:系统中若存在多个可控制总线的主模块时,其中之一若要使用总线进行数据传输时,需向系统请求总线的控制权,这就是一个总线请求的过程。8086在最小工作模式下有关总线请求的信号引脚是HOLD与HLDA。 19.简述在最小工作模式下,8086如何响应一个总线请求? 答:外部总线主控模块经HOLD引线向8086发出总线请求信号;8086在每个时钟周期的上升沿采样HOLD引线;若发现HOLD=1则在当前总线周期结束时(T4结束)发出总线请求的响应信号HLDA;8086使地址、数据及控制总线进入高阻状态,让出总线控制权,完成响应过程。 20.在基于8086的微计算机系统中,存储器是如何组织的?是如何与处理器总线连接的? BHE#信号起什么作用? 答:8086为16位处理器,可访问1M字节的存储器空间;1M字节的存储器分为两个512K字节的存储体,命名为偶字节体和奇字节体;偶体的数据线连接D7~D0,“体选”信号接地址线A0;奇体的数据线连接D15~D8,“体选”信号接BHE#信号;BHE#信号有效时允许访问奇体中的高字节存储单元,实现8086的低字节访问、高字节访问及字访问。 21.“80386是一个32位微处理器”,这句话的涵义主要指的是什么? 答:指80386的数据总线为32位,片内寄存器和主要功能部件均为32位,片内数据通路为32位。 22.80X86系列微处理器采取与先前的微处理器兼容的技术路线,有什么好处?有什么不足? 答:好处是先前开发的软件可以在新处理器组成的系统中运行,保护了软件投资。缺点是处理器的结构发展受到兼容的约束,为了保持兼容性增加了硅资源的开销,增加了结构的复杂性。 23.80386内部结构由哪几部分组成?简述各部分的作用。 答:80386内部结构由执行部件(EU)、存储器管理部件(MMU)和总线接口部件(BIU)三部分组成。EU包括指令预取部件、指令译码部件、控制部件、运算部件及保护检测部件,主要功能是执行指令。存储器管理部件包括分段部件、分页部件,实现对存储器的分段分页式的管理,将逻辑地址转换成物理地址。总线接口部件作用是进行片外访问:对存储器及I/O接口的访问、预取指令;另外的作用是进行总线及中断请求的控制 24.80386有几种存储器管理模式?都是什么? 答:80386有三种存储器管理模式,分别是实地址方式、保护方式和虚拟8086方式 25.在不同的存储器管理模式下,80386的段寄存器的作用是什么? 答:在实地址方式下,段寄存器与8086相同,存放段基地址。在保护方式下,每个段寄存器还有一个对应的64位段描述符寄存器,段寄存器作为选择器存放选择符。在虚拟8086方式下,段寄存器的作用与8086相同。 26.试说明虚拟存储器的涵义,它与物理存储器有什么区别?80386虚拟地址空间有多大? 答:虚拟存储器是程序员面对的一个巨大的、可寻址的存储空间,这个空间是内存与外存联合形成的,在操作系统的管理下,程序可象访问内存一样去访问外存而获得所需数据。物理存储器是指机器实际拥有的内存储器,不包括外存。80386的虚拟地址空间为64TB大。 27.试说明描述符的分类及各描述符的作用。 答:描述符分为三类:存储器段描述符、系统段描述符、门描述符。存储器段描述符由8字节组成,它用来说明一个段中保存信息的情况。32位段基地址和20位段界限值定位了该段在存储空间中的位置,其它有关位决定访问权限及段的长度单位。系统段描述符与存储器段描述符大多数字节段相同,有关访问权及属性字节段有些不同。门描述符用来改变程序的特权级别、切换任务的执行以及指出中断服务程序的入口。 28.描述符表的作用是什么?有几类描述符表? 答:描述符表顺序存放一系列描述符,描述符表定义了在80386系统中被使用的全部存储器段。有3类描述符表,即全局描述符表、局部描述符表及中断描述符表。 29.80386的分段部件是如何将逻辑地址变为线性地址的? 答:分段部件根据段选择符从全局描述符表或局部描述符表中取出对应的段描述符。把段描述符32位段基地址与逻辑地址中的32位偏移量相加就形成了线性地址。 30.80386中如何把线性地址变为物理地址? 答:分段部件形成的32位线性地址中高10位作为寻址页录表的偏移量,与控制寄存器CR3中页录表基地址共同形成一个32位的地址指向页表中的一个页项,即为一个页面描述符。该页面项中高20位作为页面基地址,线性地址的低12位为偏移量,相加后形成指向某一存储单元的32位物理地址。若禁止分页功能,线性地址就是物理地址。 31.80386对中断如何分类? 答:80386把中断分为外部中断和内部中断两大类,外部中断经NMI和INTR引线输入请求信号。内部中断也叫内部异常中断,分为陷阱中断、内部故障异常中断、异常终止中断。 32.80386在保护方式下中断描述符表与8086的中断向量表有什么不同? 答:8086工作在实地址方式,向量表是在存储器的0段中最低1024字节内存中。80386在保护方式下要通过中断描述符表中的描述符访问虚拟空间的中断向量,中断描述符表的位置不是固定的,要由IDTR寄存器实现在虚拟空间的定位。 33.简述80386在保护方式下的中断处理过程。 答:80386响应中断后,接收由中断源提供的类型码并将其乘8,与IDTR寄存器中基地址相加,指出中断描述符的位置,读出中断描述符,依其中的段选择符及条件决定从两个描述符表LDT或GDT中的一个得到段描述符,形成中断服务程序入口所在存储器单元的线性地址。 第3章 8086指令系统及寻址方式
数据库系统原理练习1 一、单项选择题(本大题共15小题,每小题2分,共30分) 在每小题列出的四个备选项中只有一个是符合题要求的,请将其代码填写在题后的 括号内。错选、多选或未选均无分。 1.信息世界中的术语"实体"对应于机器世界的术语是( ) A.记录 B.字段 C.文件 D.关键码 2.用二维表结构表达实体集的模型是( ) A.概念模型 B.层次模型 C.网状模型 D.关系模型 3.内模式的修改尽量不影响概念模式的特点称为数据库的( ) A.物理数据独立性 B.逻辑数据独立性 C.外模式数据独立性 D.内模式数据独立性 4.设R和S是任意两个关系,则RS等价于( ) A.(R-S) S B.R (R-S) C.S-(R-S) D.R-(R-S) 5.设有R与S两个关系如题5图所示: 关系代数表达式 C,E( (R s))的结果是( ) A.c1,el B.c2,e2 C.c3,e2 D. c3,e1 6.SQL语言中,HAVING子句用于筛选满足条件的( ) A.列 B.行 C.分组 D.元组 7.若R 1NF,且R中只有一个主属性,则R必然满足( ) A.2NF B.3NF C.4NF D.BCNF 8.设有关系模式R(A,B,C,D),F是R上成立的FD集,F={B C,C D},则属性C的闭包C +为( ) A.BC B.BCD C.BD D.CD 9.将ER模型转换成关系模型的过程属于数据库的( ) A.需求分析 B. 概念设计 C.逻辑设计 D. 物理设计 10.有职工和亲属两个关系,当职工调出时,应该从职工关系中删除该职工的元组,同 时应该从亲属关系中删除该职工所有亲属的元组,在SQL语言中,定义这个完整性约束的 短语是( ) A. ON DELETE ALL B.ON DELETE CASCADE C.ON DELETE RESTRICT D.ON DELETE SET NULL 11.多个事务执行的次序称为( ) A.过程 B.流程 C.调度 D.步骤 12.断言机制主要用于( ) A.数据库恢复 B.完整性控制 C.并发控制 D. 安全性控制 13.在DBMS中,实现事务持久性的是( ) A.完整性子系统 B. 安全性子系统 C. 恢复子系统 D. 并发控制子系统 14.在分布式数据库系统中,将数据库划分为若干不相交的逻辑子集,且每个场地存储 一个逻辑子集的数据分配策略称为( ) A.集中式 B.分割式 C.混合式 D.组合式 15.在面向对象数据库系统中,对象定义采用的信息隐蔽技术称为( ) A.保密性 B.封装性 C.自闭性 D.隐蔽性 二、填空题(本大题共10小题,每小题1分,共10分) 请在每小题的空格中填上正确答案。错填、不填均无分。 16.数据库的三级模式结构中,单个用户使用的数据视图描述称为___________。 17.在数据库系统中三级模式结构的定义存放于___________中。 18.关系模型有三类完整性约束:实体完整性、用户定义的完整性和___________。 19.设有关系R和S,与元组关系演算表达式{t"R(t) S(t)}等价的关系代数表达式是__ _________。 20.设关系R(A,B,C)和S(A,D),用关系代数的基本运算写出与R S等价的关系代数表达式是___________。 21.设有关系模式R(A,B,C,D),F是R上成立的FD集,F={A C,A D},则R的候选键是 ___________。 22.如果X Y且有YX,那么X Y称为___________。 23.在SQL语言中,用于测试一个关系是否非空的谓词是___________。 24.在数据库系统中,视图的作用是简化操作、提高数据的独立性和数据的__________ _。 25.各场地的数据模型不同的分布式数据库系统称为___________DDBS。 三、简答题(本大题共10小题,每小题3分,共30分) 26.有学生关系S和成绩关系SC,如题26图所示,写出S与SC左外联接运算的结果。 27.依据题26图,写出与SQL语句: SELECT sname FROM S WHERE sno IN (SELECT sno FROM SC) 等价的关系代数表达式。 28.设关系模式R(A,B,C),F={AC B,AB C,B C},则R最高属于第几范式?说明理 由。 29.写出Armstrong推理规则中自反律、增广律、传递律的形式化定义。 30.简述对嵌入式SQL进行预处理的作用和过程。 31.简述日志文件的内容。 32.简述SQL中的安全性控制机制。 33.已知事务T1和T2的并发操作序列,如题33图所示,指出这个并发操作引发的问题。 34.在面向对象数据库中,查找对象的方法有哪三种? 35

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值