AcWing 92. 递归实现指数型枚举
从 1~n 这 n 个整数中随机选取任意多个,输出所有可能的选择方案。
输入格式
输入一个整数n。
输出格式
每行输出一种方案。
同一行内的数必须升序排列,相邻两个数用恰好1个空格隔开。
对于没有选任何数的方案,输出空行。
本题有自定义校验器(SPJ),各行(不同方案)之间的顺序任意。
数据范围
1≤n≤15
输入样例:
3
输出样例:
3
2
2 3
1
1 3
1 2
1 2 3
思路
对于指数型枚举,每个数只有两种选择:一个是选,一个是不选;看见递归,首先要把递归转为一颗递归搜索树,用树的思想来看递归,题目要求同一行内的数必须升序,那么我们就从小到大开始遍历,这里又两种枚举顺序:1、依次枚举每个数,放哪个位置;2、依次枚举每个位置,放哪个数。这里我选择第二种顺序。整数n相当于是n个坑,我们依次枚举每个坑,然后选择某个数放进去,同时我们使用一个整形数组st[N]来记录每个数的状态,未枚举到的记为0,选中的记为1,不选的记为2,每次递归都要记得还原现场,补充一个知识点:全局变量初值为0,布尔类型的初值为false,局部变量的初值是随机值。
递归的注意事项:
1、先确定边界(记得return);
2、一定要恢复现场(父节点)
代码
import java.io.*;
public class Main{
static final int N = 16;
static int n, cnt;
static int[] st = new int[N];
static int[][] pro = new int[1<<15][N];
static BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
static void dfs(int u) throws Exception{
if(u > n){
for(int i = 1; i <= n; i ++){
if(st[i] == 1){
pro[cnt][i] = i;
}
}
cnt ++;
return;
}
st[u] = 2;
dfs(u + 1);
st[u] = 0;
st[u] = 1;
dfs(u + 1);
st[u] = 0;
}
public static void main(String[] args) throws Exception {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
n = Integer.parseInt(br.readLine());
dfs(1);
for(int i = 0; i < cnt; i ++){
for(int j = 1; j <= n; j ++){
if(pro[i][j] != 0)
bw.write(pro[i][j] + " ");
}
bw.write("\n");
}
bw.flush();
bw.close();
br.close();
}
}
AcWing 94. 递归实现排列型枚举
把 1~n 这 n 个整数排成一行后随机打乱顺序,输出所有可能的次序。
输入格式
一个整数n。
输出格式
按照从小到大的顺序输出所有方案,每行1个。
首先,同一行相邻两个数用一个空格隔开。
其次,对于两个不同的行,对应下标的数一一比较,字典序较小的排在前面。
数据范围
1≤n≤9
输入样例:
3
输出样例:
1 2 3
1 3 2
2 1 3
2 3 1
3 1 2
3 2 1
思路
排列型枚举,需要把这n个数不同的顺序枚举出来,题意是要按从小到大的顺序输出所有方案,我们只需要在枚举的时候从小到大进行枚举,这样就可以保证输出的方案也是从小到大的顺序,同样也是用枚举每个位置放哪个数的枚举方式,把递归转化为递归搜索树,需要一个状态数组来避免数字重复出现,0表示没用过,1表示用过
代码1
import java.io.*;
import java.util.Scanner;
public class Main{
static final int N = 10;
static int n;
static boolean[] st = new boolean[N];//false表示没用过,true表示用过了
static int[] pro = new int[N];
static BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
static void dfs(int u) throws Exception{
if(u > n){
for(int i = 1; i <= n; i ++){
bw.write(pro[i] + " ");
}
bw.write("\n");
return;
}
for(int i = 1; i <= n; i ++){
if(!st[i]){
st[i] = true;
pro[u] = i;
dfs(u + 1);
//恢复现场
st[i] = false;
pro[u] = 0;
}
}
}
public static void main(String[] args) throws Exception{
Scanner sc = new Scanner(System.in);
n = sc.nextInt();
dfs(1);
bw.flush();
bw.close();
}
}
代码2
用二进制来表示用没用过(状态压缩)
import java.io.*;
public class Main{
static int final N = 10;
static int n;
static int[] pro = new int[N];
static BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
static BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
static void dfs(int u, int index) throws Exception{
if(u > n){
for(int i = 1, i <= n; i ++){
bw.write(pro[i] + " ");
}
bw.write("\n");
return;
}
for(int i = 0; i < n; i ++){
if(((index >> i) & 1) == 0){
pro[u] = i + 1;
dfs(u + 1, index | (1 << i));
}
}
}
public static void main(String[] args) throws Exception{
n = Integer.parseInt(br.readLine());
dfs(1, 0);
bw.flush();
bw.close();
br.close();
}
}
AcWing 93. 递归实现组合型枚举
从 1~n 这 n 个整数中随机选出 m 个,输出所有可能的选择方案。
输入格式
两个整数 n,m ,在同一行用空格隔开。
输出格式
按照从小到大的顺序输出所有方案,每行1个。
首先,同一行内的数升序排列,相邻两个数用一个空格隔开。
其次,对于两个不同的行,对应下标的数一一比较,字典序较小的排在前面(例如1 3 5 7排在1 3 6 8前面)。
数据范围
n>0 ,
0≤m≤n ,
n+(n−m)≤25
输入样例:
5 3
输出样例:
1 2 3
1 2 4
1 2 5
1 3 4
1 3 5
1 4 5
2 3 4
2 3 5
2 4 5
3 4 5
思路
与排列型枚举不同,组合型枚举不要求顺序,123和321都是一样的组合,且题目要求要从小到大的顺序输出,我们可以从小开始枚举,并且后面的数必须要比前一个数大,我们可以枚举完一个数后,继续从这个数的下一个数开始枚举,这样既保证了顺序,也保证了组合不重复。
代码
import java.io.*;
public class Main {
static final int N = 30;
static int n, m;
static int[] path = new int[N];
static StreamTokenizer st = new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in)));
static BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
static void dfs(int u, int s) throws IOException {
if (u + n - s + 1 < m) {
return;
}
if (u == m) {
for (int i = 0; i < m; ++i) {
bw.write(path[i] + " ");
}
bw.write("\n");
return;
}
for (int i = s; i <= n; ++i) {
path[u] = i;
dfs(u + 1, i + 1);
}
}
public static void main(String[] args) throws IOException {
// st = new StreamTokenizer(
// new BufferedReader(new InputStreamReader(new FileInputStream("/Users/huangweixuan/testdata.txt"))));
st.nextToken();
n = (int)st.nval;
st.nextToken();
m = (int)st.nval;
dfs(0, 1);
bw.close();
}
}
AcWing 717. 简单斐波那契
以下数列0 1 1 2 3 5 8 13 21 …被称为斐波纳契数列。
这个数列从第3项开始,每一项都等于前两项之和。
输入一个整数N,请你输出这个序列的前N项。
输入格式
一个整数N。
输出格式
在一行中输出斐波那契数列的前N项,数字之间用空格隔开。
数据范围
0<N<46
输入样例:
5
输出样例:
0 1 1 2 3
思路
这题很简单,是递推,这里区别一下递归,递归是先算整体再算局部然后局部返回的结果给上一层,而递推是先算局部再算整体,这道题是求斐波那契数列的前N项,可以用数组来保存结果,也可以用两个变量保存(可以省点空间)
代码1——数组
import java.io.*;
import java.util.Scanner;
public class Main{
static final int N = 50;
static int n;
static int[] pro = new int[N];
public static void main(String[] args) throws Exception{
Scanner sc = new Scanner (System.in);
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
n = sc.nextInt();
pro[0] = 0;
pro[1] = 1;
for(int i = 2; i < n; i ++){
pro[i] = pro[i - 1] + pro[i - 2];
}
for(int i = 0; i < n; i ++){
bw.write(pro[i] + " ");
}
bw.flush();
bw.close();
}
}
代码2——变量
import java.io.*;
public class Main{
public static void main(String[] args) throws Exception{
StreamTokenizer st = new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in)));
//BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
st.nextToken();
int n = (int)st.nval;
int a = 0, b = 1, f = 0;
for(int i = 0; i < n; i++){
//bw.write(a + " "); 在输出数据量不大的情况下,与System.out.print()的输出速率差不多,建议数据量不大用System.out.print(),这样可以省写了BufferedWriter类的调用,减少代码量
System.out.print(a + " ");
f = a + b;
a = b;
b = f;
}
//bw.flush();
//bw.close();
}
}
AcWing 95. 费解的开关
你玩过“拉灯”游戏吗?
25 盏灯排成一个 5×5 的方形。
每一个灯都有一个开关,游戏者可以改变它的状态。
每一步,游戏者可以改变某一个灯的状态。
游戏者改变一个灯的状态会产生连锁反应:和这个灯上下左右相邻的灯也要相应地改变其状态。
我们用数字 1 表示一盏开着的灯,用数字 0 表示关着的灯。
下面这种状态
10111
01101
10111
10000
11011
在改变了最左上角的灯的状态后将变成:
01111
11101
10111
10000
11011
再改变它正中间的灯后状态将变成:
01111
11001
11001
10100
11011
给定一些游戏的初始状态,编写程序判断游戏者是否可能在 6 步以内使所有的灯都变亮。
输入格式
第一行输入正整数 n,代表数据中共有 n 个待解决的游戏初始状态。
以下若干行数据分为 n 组,每组数据有 5 行,每行 5 个字符。
每组数据描述了一个游戏的初始状态。
各组数据间用一个空行分隔。
输出格式
一共输出 n 行数据,每行有一个小于等于 6 的整数,它表示对于输入数据中对应的游戏状态最少需要几步才能使所有灯变亮。
对于某一个游戏初始状态,若 6 步以内无法使所有灯变亮,则输出 −1。
数据范围
0<n≤500
输入样例:
3
00111
01011
10001
11010
11100
11101
11101
11110
11111
11111
01111
11111
11111
11111
11111
输出样例:
3
2
-1
思路
这道题看似很复杂,其实很简单。每一个开关会影响上下左右的开关,每一个开关按2次相当于没有按,所以说与顺序无关,且每个开关最多按1次,当第i行遍历完之后,想要改变这第i行的话,只能由第i + 1 行来改变,所以其实就是第一行的变换决定了后面几行的变换,所以第一行有多少种方案,结果就有多少种方案。
代码
import java.io.*;
import java.util.Scanner;
public class Main{
static final int N = 5;
static char[][] ans = new char[N][N];
static char[][] backup = new char[N][N];
static int[] dx = {0,-1,0,1,0};
static int[] dy = {0,0,1,0,-1};
//static Scanner sc = new Scanner(System.in);
static void turn(int x, int y) throws Exception{
for(int i = 0;i < 5;i++)
{
int a = x + dx[i];
int b = y + dy[i];
if(a >= 0 && a < 5 && b >= 0 && b < 5)
{
ans[a][b] ^= 1;
}
}
}
public static void main(String[] args) throws Exception{
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
int n = Integer.parseInt(br.readLine());
//int n = sc.nextInt();//不能与br混用
while(n -- > 0){
for(int i = 0; i < 5; i ++){
ans[i] = br.readLine().toCharArray();
}
if (n > 0) br.readLine(); //用于接收每组数据后面的空行
int count = 10;
for(int op = 0; op < 1 << 5; op ++){
for(int i = 0; i < 5; i ++){
//backup[i] = ans[i].clone();//这个是浅拷贝
System.arraycopy(ans[i], 0, backup[i], 0, N);
}
int res = 0;
for(int i = 0; i < 5; i ++){
if((op >> i & 1) == 1){
res ++;
turn(0, i);
}
}
for(int i = 0; i < 4; i ++){
for(int j = 0; j < 5; j ++){
if(ans[i][j] == '0'){
res ++;
turn(i + 1, j);
}
}
}
boolean check = false;
for(int j = 0; j < 5; j ++){
if(ans[4][j] == '0'){
check = true;
break;
}
}
if(!check) count = Math.min(res, count);
for(int i = 0; i < 5; i ++){
//ans[i] = backup[i].clone();
System.arraycopy(backup[i], 0, ans[i], 0, 5);
}
}
if(count > 6) count = -1;
System.out.println(count);
}
}
}
教训
1、写这个费解的开关,遇到了很坑的问题,就是Scanner与io的BufferedReader不能混用,混用Scanner就报错。
2、在用BufferedReader 输入的时候,忽略了每组数据后面还有以后"\n",然后导致char数组接收值时超出了边界,"\n"也被当成是输入
last source index 5 out of bounds for char[0]
AcWing 116. 飞行员兄弟
“飞行员兄弟”这个游戏,需要玩家顺利的打开一个拥有 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
思路
方法一:
这个有点类似“费解的开关”但也有不同之处:飞行员兄弟不受前面的开关影响,所以说每个开关都是可以按的,每个开关有两种状态,一是按、一是不按,也就是2^16种方案
1、枚举所有方案 用二进制进行枚举, 0 ~ 2 ^ (16 - 1)
2、按照该方案对所有灯泡进行操作
3、判断灯泡是否全亮,全亮则记录方案
细节:先计算时间复杂度: 2 ^16 * ( 16 * 7 + 16 + 16) = 9,437,184 不到一亿可以接收。(对每一个方案都进行操作,然后枚举每一个灯泡(*16),每个灯泡会影响7个灯泡(*7),然后判断每个灯泡是否全亮(+ 16),如果全亮则记录方案(+ 16))。
代码
import java.io.*;
public class Main{
static final int N = 5;
static char[][] m = new char[N][N];
static char[][] backup = new char[N][N];
static void turn_one(int i, int j){
if(m[i][j] == '-')m[i][j] ='+';
else m[i][j] ='-';
}
static void turn(int i, int j){
for(int k = 0; k < 4; k ++){
turn_one(i, k);
turn_one(k, j);
}
turn_one(i, j);
}
public static void main(String[] args) throws Exception{
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
for(int i = 0; i < 4; i ++){
m[i] = br.readLine().toCharArray();
}
int res = Integer.MAX_VALUE;
String Y = "";
for(int op = 0; op < 1 << 16; op ++){
for(int i = 0; i < 4; i ++){
backup[i] = m[i].clone();
}
int ans = 0;
String T = "";
for (int i = 0; i < 4; i ++){
for (int j = 0; j < 4; j ++){
if(((op >> (i * 4 + j)) & 1) == 1){
ans ++;
turn(i, j);
T += (i + 1) + " " + (j + 1) + "\n";
}
}
}
boolean dark = false;
for(int a = 0; a < 4; a ++){
for(int b = 0; b < 4; b ++){
if(m[a][b] == '+'){
dark = true;
break;
}
}
}
if(!dark){
res = Math.min(res, ans);
Y = T;
}
for(int i = 0; i < 4; i ++){
m[i] = backup[i].clone();
}
}
System.out.println(res);
System.out.print(Y);
}
}
方法二:二进制写法(略,有空再补充学习)
方法三:dfs(略,有空再补充学习,这个方法ac巨快)
AcWing 1208. 翻硬币
小明正在玩一个“翻硬币”的游戏。
桌上放着排成一排的若干硬币。我们用 * 表示正面,用 o 表示反面(是小写字母,不是零)。
比如,可能情形是:oo*oooo
如果同时翻转左边的两个硬币,则变为:oooo***oooo
现在小明的问题是:如果已知了初始状态和要达到的目标状态,每次只能同时翻转相邻的两个硬币,那么对特定的局面,最少要翻动多少次呢?
我们约定:把翻动相邻的两个硬币叫做一步操作。
输入格式
两行等长的字符串,分别表示初始状态和要达到的目标状态。
输出格式
一个整数,表示最小操作步数
数据范围
输入字符串的长度均不超过100。
数据保证答案一定有解。
输入样例1:
**********
o****o****
输出样例1:
5
输入样例2:
*o**o***o***
*o***o**o***
输出样例2:
1
思路
这个跟费解的开关思路一样,是费解的开关的降维版本,同样是前面的硬币会影响后面的硬币,所以说,其实这题的答案是唯一的,不存在更多解。
代码
import java.io.*;
public class Main{
static final int N = 100;
static char[][] m = new char[2][N];//用一个二维数组来保存输入,第一行为初始态,第二行为目标态
static void turn_one(int i){
if(m[0][i] == '*') m[0][i] ='o';
else m[0][i] = '*';
}
static void turn(int i, int j){
turn_one(i);
turn_one(j);
}
public static void main(String[] args) throws Exception{
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
for(int i = 0; i < 2; i ++){
m[i] = br.readLine().toCharArray();
}
int l = m[0].length;
int res = 0;
for(int i = 0; i < (l - 1); i ++){
if(m[0][i] != m[1][i]){
res ++;
turn(i, i+1);
}
}
System.out.print(res);
}
}