本片博客主要介绍了用递推来解决问题,并通过讲解二分的各种情况和步骤,让大家熟悉如何使用二分,并以数的范围这道题来解释。
前言
本片博客主要介绍了用递推来解决问题,并通过讲解二分的各种情况和步骤,让大家熟悉如何使用二分,并以数的范围这道题来解释。
飞行员兄弟
“飞行员兄弟”这个游戏,需要玩家顺利的打开一个拥有 16 个把手的冰箱。
已知每个把手可以处于以下两种状态之一:打开或关闭。
只有当所有把手都打开时,冰箱才会打开。
把手可以表示为一个 4×4 的矩阵,您可以改变任何一个位置 [i,j] 上把手的状态。
但是,这也会使得第 i行和第 j 列上的所有把手的状态也随着改变。
请你求出打开冰箱所需的切换把手的次数最小值是多少。
输入格式
输入一共包含四行,每行包含四个把手的初始状态。
符号 +
表示把手处于闭合状态,而符号 -
表示把手处于打开状态。
至少一个手柄的初始状态是关闭的。
输出格式
第一行输出一个整数 N,表示所需的最小切换把手次数。
接下来 N 行描述切换顺序,每行输出两个整数,代表被切换状态的把手的行号和列号,数字之间用空格隔开。
注意:如果存在多种打开冰箱的方式,则按照优先级整体从上到下,同行从左到右打开。
数据范围
1≤i,j≤4
输入样例:
-+--
----
----
-+--
输出样例:
6
1 1
1 3
1 4
4 1
4 3
4 4
算法思路
我们可以直接暴力枚举所有的方案,4行4列,那么共有16个位置,每个位置都有操作和不操作两种方案,那么最后共有2^16 = 65536种方案,枚举每一种方案,当最后的冰箱全是打开状态,然后将每一种方案操作的行列数记录,最后将每一种方案的次数进行比较,最后得到次数最小的方案即是最后的结果。
用二维字符数组grap来存储初始数据,因为要枚举65536种方案,我们再使用一个二维字符数组backup来备份数据。然后用op来表示操作,枚举65536次,其中我们把二维的坐标用一维来表示,然后再用二进制数字某个特定为是否为1。(具体只是为了枚举65536种方案,我们该对哪一个位置进行修改或不修改操作)将操作对应的行和列号用来Pair来存储,放到一个列表list中。
跟(《从入门到精通:蓝桥杯编程大赛知识点全攻略》(三)-费解的开关、翻硬币-优快云博客)中费解的开关的op性质一样,可以参考一下这篇博客
将二维转换成一维数字表示get(int x,int y)函数,具体表示为。
用一个布尔类型变量close来表示所有的冰箱是否处于打开状态,true为存在冰箱有关闭状态,false为全打开状态。因为题目中没有说所有数据都存在答案,我们检测一下,存在关闭就说明该方案行不通。当处于全打开状态,我们就需要最后储存答案的列表res和每一种方案存储改变行号和列号的步骤的列表list作比较,最后要得到改变行号和列好次数最少的答案。
其中turn_all(int x,int y)函数是来改变坐标(x,y)和所在行和列,做一个循环分别改变行和列,因为行和列都改变坐标(x,y),所以相当于没改变,最后还要再改变一次(x,y)本身。
代码如下
import java.io.*;
import java.util.ArrayList;
import java.util.Arrays;
public class Main {
static PrintWriter pw = new PrintWriter(new OutputStreamWriter(System.out));
static BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
static StreamTokenizer st = new StreamTokenizer(br);
static int N = 5;
static char[][] grap = new char[N][N];
static char[][] backup = new char[N][N];
public static void main(String[] args) throws IOException {
for(int i = 0;i < 4;i++){
grap[i] = nextLine().toCharArray();
}
ArrayList<Pair> res = new ArrayList<Pair>();
for(int op = 0;op < 65536;op++){
//备份
for(int i = 0;i < 4;i++){
backup[i] = Arrays.copyOf(grap[i], grap[i].length);
}
ArrayList<Pair> list = new ArrayList<Pair>();
//进行操作
for(int i = 0;i < 4;i++){
for(int j = 0;j < 4;j++){
if((op >> get(i,j) & 1) == 1){
list.add(new Pair(i,j));
turn_all(i,j);
}
}
}
boolean close = false;
//判断冰箱是否为全打开
for (int i = 0;i < 4;i++){
for(int j = 0;j < 4;j++){
if(grap[i][j] == '+'){
close = true;
}
}
}
if(!close){
if(res.isEmpty() || res.size() > list.size()){
res = list;
}
}
//还原
for(int i = 0;i < 4;i++){
grap[i] = Arrays.copyOf(backup[i], backup[i].length);
}
}
pw.println(res.size());
for(int i = 0;i < res.size();i++){
pw.println((res.get(i).x+1)+" "+(res.get(i).y+1));
}
pw.flush();
}
//将二维数组转换成一维
public static int get(int x,int y){
return 4 * x + y;
}
public static void turn_all(int x,int y){
for(int i = 0;i < 4;i++){
turn_one(x,i);
turn_one(i,y);
}
turn_one(x,y);
}
public static void turn_one(int x,int y){
if(grap[x][y] == '+'){
grap[x][y] = '-';
}else {
grap[x][y] = '+';
}
}
public static String nextLine() throws IOException {
return br.readLine();
}
}
class Pair{
int x;
int y;
public Pair(int x,int y){
this.x = x;
this.y = y;
}
}
二分
上述两种方法写二分就可以避免死循环的出现。
第一种情况:ans在M的右边,当L = R - 1,即说明区间只有两个元素,此时M = ( L + R ) / 2为L,但正确答案确是R,满足if条件时 L = M = L,会一直卡在这,成为死循环,所以 M = (L + R + 1) / 2就就可以避免这种情况。
整数二分步骤:
1.找一个区间[L,R],使得答案一定在该区间中
2.找一个判断条件,使得该判断条件具有二段性,并且答案一定是该二段性的分界点
3.分析中点M在该判断条件下是否成立,如果成立,考虑在哪个区间;如果不成立,考虑答案在哪个区间。
4.如果更新方式写的是R=M,则不需要做任何操作;更新方式写的是L = M,则需要在计算M的时候加上1。
数的范围
给定一个按照升序排列的长度为 n 的整数数组,以及 q 个查询。
对于每个查询,返回一个元素 k 的起始位置和终止位置(位置从 0 开始计数)。
如果数组中不存在该元素,则返回 -1
。
输入格式
第一行包含整数 n 和 q,表示数组长度和询问个数。
第二行包含 n 个整数(均在 1∼10000 范围内),表示完整数组。
接下来 q 行,每行包含一个整数 k,表示一个询问元素。
输出格式
共 q 行,每行包含两个整数,表示所求元素的起始位置和终止位置。
如果数组中不存在该元素,则返回 -1 -1
。
数据范围
1≤n≤100000
1≤q≤10000
1≤k≤10000
输入样例
6 3
1 2 2 3 3 4
3
4
5
输出样例
3 4
5 5
-1 -1
算法思路
主要分两部分,分别找区间的左端点和右端点。
用整型数组arr来存储数组,左端点left = 0,右端点right = n - 1,mid = (left + right)/ 2,当
arr[mid] >= x说明mid及其左边可能含有值为x的元素,把右边界左移;当 arr[mid] < x说明此时左边及arr[mid]不可能存在x,所以左边界右移,left = mid + 1;最后当left = right,此时我们得到的arr[left]所在的若是x一定是x元素出现的最小位置,我们就得到了区间的左端点。
当arr[mid] <= x,说明mid及其右边可能含有值为x的元素,所以左端点右移;当arr[mid] > x,说明待查元素只会在mid的左边,right = mid - 1;最后当right = left时,arr[right]所在的若是x一定是x元素出现的最大位置我们就得到了右端点。
代码如下
package AcWingLanQiao;
import java.io.*;
public class 数的范围 {
static PrintWriter pw = new PrintWriter(new OutputStreamWriter(System.out));
static BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
static StreamTokenizer st = new StreamTokenizer(br);
static int[] arr = new int[100010];
public static void main(String[] args)throws Exception {
int n = nextInt();
int m = nextInt();
for(int i=0;i<n;i++){
arr[i] = nextInt();
}
while(m-->0){
int x = nextInt();
//二分x的左端点
int left = 0;
int right = n - 1;
while(left<right){
int mid = (left+right)/2;
if(arr[mid] >= x){
right = mid;
}else {
left = mid + 1;
}
}
if(arr[left] == x){
pw.print(left+" ");
right = n - 1;
while(left<right){
int mid = (left+right + 1)/2;
if(arr[mid] <= x){
left = mid ;
}else {
right = mid - 1;
}
}
pw.println(right);
}
else{
pw.println("-1 -1");
}
}
pw.flush();
}
public static int nextInt()throws Exception{
st.nextToken();
return (int)st.nval;
}
}
总结
希望这篇博客让大家更好的了解递推和二分,解决遇到的一类问题!!!