前言
在本篇博客中,我们将深入探讨三道经典的算法题目——《红与黑》、《交换瓶子》和《完全二叉树的权值》。这些问题涵盖了不同的算法技巧和数据结构应用,帮助我们更好地理解复杂问题的解决方案。通过逐步分析每一道题目,我们将不仅学习到如何实现这些算法,还能掌握其中的关键思路和优化策略。希望通过这些内容,能够提升大家在算法竞赛和实际编程中的能力。
红与黑
有一间长方形的房子,地上铺了红色、黑色两种颜色的正方形瓷砖。
你站在其中一块黑色的瓷砖上,只能向相邻(上下左右四个方向)的黑色瓷砖移动。
请写一个程序,计算你总共能够到达多少块黑色的瓷砖。
输入格式
输入包括多个数据集合。
每个数据集合的第一行是两个整数 W
和 H,分别表示 x 方向和 y方向瓷砖的数量。
在接下来的 H 行中,每行包括 W 个字符。每个字符表示一块瓷砖的颜色,规则如下:
- ‘.’:黑色的瓷砖;
- ‘#’:红色的瓷砖;
- ‘@’:黑色的瓷砖,并且你站在这块瓷砖上。该字符在每个数据集合中唯一出现一次。
当在一行中读入的是两个零时,表示输入结束。
输出格式
对每个数据集合,分别输出一行,显示你从初始位置出发能到达的瓷砖数(记数时包括初始位置的瓷砖)。
数据范围
1≤W,H≤20
输入样例:
6 9
....#.
.....#
......
......
......
......
......
#@...#
.#..#.
0 0
输出样例:
45
算法思路
二维字符数组map用来记录地图。
flag[i]表示该位置是否被走过,true为走过,false为未走。
这道题需要注意输入,因为有多个样例且未说样例个数,给出了结束标志,需要我们自己去判断结束。如果flag数组用的全局变量的话,需要每一个测试样例结束进行清空。
先循环map找到起点,类Pair用来存储横纵坐标,start表示位置的开始同时记录结果的res = 1(带上开始的位置),同时将该位置的flag[i][j] = true表示走过。
接下来进行bfs宽度优先搜索。
设置一个队列,将开始位置放入其中,然后做一个循环,当队列不为空时,弹出队尾元素,然后进行上下左右操作,接下来进行判断是否越界、是否是黑砖、是否已经走过即(x >= 0 && x < row && y >= 0 && y < col && map[x][y] == ‘.’ && !flag[x][y]),当条件满足后,砖数加1即res++,将该位置设置为走过即flag[x][y] = true,将该位置再放入队列即(q.add(new Pair(x,y)));当循环结果后返回黑砖的数目即res即可。
代码如下
import java.io.*;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.Queue;
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 row,col;
static int N = 25;
static char[][] map = new char[N][N];
static boolean[][] flag = new boolean[N][N];
public static void main(String[] args) throws Exception{
Pair start = new Pair();
int res = 0;
while(true){
String[] s = br.readLine().split(" ");
row = Integer.parseInt(s[1]);
col = Integer.parseInt(s[0]);
if(row == 0 && col == 0) break;
for(int i = 0 ; i < N ; i++){
Arrays.fill(flag[i], false);//将flag数组恢复为false
}
for(int i = 0;i < row;i++){
String str = br.readLine();
for(int j = 0;j < col;j++){
map[i][j] = str.charAt(j);
if(map[i][j] == '@'){
start = new Pair(i,j);
flag[i][j] = true;
}
}
}
res = bfs(start);
pw.println(res);
}
pw.flush();
}
public static int bfs(Pair start){
int res = 1;
Queue<Pair> q = new LinkedList<>();
q.add(start);
int[] dx = {1, -1, 0, 0};
int[] dy = {0, 0, -1, 1};
while(!q.isEmpty()){
Pair p = q.poll();
for(int i = 0;i < 4;i++){
int x = p.x + dx[i];
int y = p.y + dy[i];
if(x >= 0 && x < row && y >= 0 && y < col && map[x][y] == '.' && !flag[x][y]) {
res++;
flag[x][y] = true;
q.add(new Pair(x,y));
}
}
}
return res;
}
public static int nextInt() throws Exception{
st.nextToken();
return (int)st.nval;
}
public static String nextLine()throws Exception {
return br.readLine();
}
static class Pair{
int x;
int y;
public Pair(){}
public Pair(int x, int y){
this.x = x;
this.y = y;
}
}
}
交换瓶子
有 N 个瓶子,编号 1∼N,放在架子上。
比如有 5 个瓶子:
2 1 3 5 4
要求每次拿起 2个瓶子,交换它们的位置。
经过若干次后,使得瓶子的序号为:
1 2 3 4 5
对于这么简单的情况,显然,至少需要交换 2 次就可以复位。
如果瓶子更多呢?你可以通过编程来解决。
输入格式
第一行包含一个整数 N,表示瓶子数量。
第二行包含 N 个整数,表示瓶子目前的排列状况。
输出格式
输出一个正整数,表示至少交换多少次,才能完成排序。
数据范围
1≤N≤10000,
输入样例1:
5
3 1 2 5 4
输出样例1:
3
输入样例2:
5
5 4 3 2 1
输出样例2:
2
算法思路
暴力做法:
通过观察发现每一个数都必须回到自己的位置上,例如1必须在第一个位置、2必须在第二个位置依次类推。
那么就通过循环遍历数组,当前数组位置不对即arr[i] != i,就进行内层循环j从i+1开始,找到arr[j] == i,然后进行两个数字交换,然后将交换次数相加。
我们可以将每个数字看作是一个点,该瓶子向所在的位置序号与该瓶子的序号相等的瓶子相连即3->2、2->1、1->3;接下来交换两个瓶子只会存在两种情况(1)交换同一个环的点,例如交换瓶子1和瓶子3,最后会变成1的自环和2、3的环,故环内的点交换必将裂成两个环(2)交换不同环中的点,例如交换2和4,最后发现两个环会变成一个环故交换不同环中的点必将两个环合成一个环。
最后我们需要形成一个有序的数列,那么必然是n个环,所以需要统计现在数组中有多少个环cnt,最后需要操作的次数就是(n-cnt)。
我们需要一个布尔类型数组flag,flag[i]表示该点是否在已经统计的环里面,循环遍历每一个点i,然后判断该点flag[i] = false,表示该点还未被统计此时cnt++,接下来再遍历所在环中的每一个点,将环中的店flag[j] = true;最后输出n-cnt即可。
代码如下
暴力做法代码如下:
import java.io.*;
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 = 10010;
static int n;
static int[] arr = new int[N];
public static void main(String[] args) throws Exception{
n = nextInt();
for(int i = 1;i <= n;i++){
arr[i] = nextInt();
}
int cnt = 0;
for(int i = 1;i <= n;i++){
if(arr[i] != i){
for(int j = i + 1;j <= n;j++){
if(arr[j] == i){
int temp = arr[j];
arr[j] = arr[i];
arr[i] = temp;
cnt++;
}
}
}
}
pw.println(cnt);
pw.flush();
}
public static int nextInt()throws Exception{
st.nextToken();
return (int)st.nval;
}
}
import java.io.*;
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 = 10010;
static int n;
static int[] arr = new int[N];
static boolean[] flag = new boolean[N];
public static void main(String[] args) throws Exception{
n = nextInt();
for(int i = 1;i <= n;i++){
arr[i] = nextInt();
}
int cnt = 0;
for(int i = 1;i <= n;i++){
//统计环的数量
if(!flag[i]){
cnt++;
for(int j = i;!flag[j];j = arr[j]){
flag[j] = true;
}
}
}
pw.println(n-cnt);
pw.flush();
}
public static int nextInt()throws Exception{
st.nextToken();
return (int)st.nval;
}
}
完全二叉树的权值
给定一棵包含 N 个节点的完全二叉树,树上每个节点都有一个权值,按从上到下、从左到右的顺序依次是 A1,A2,⋅⋅⋅AN,如下图所示:
现在小明要把相同深度的节点的权值加在一起,他想知道哪个深度的节点权值之和最大?如果有多个深度的权值和同为最大,请你输出其中最小的深度。
注:根的深度是 1。
输入格式
第一行包含一个整数 N。
第二行包含 N 个整数 A1,A2,⋅⋅⋅AN。
输出格式
输出一个整数代表答案。
数据范围
1≤N≤105,
−105≤Ai≤105
输入样例:
7
1 6 5 4 3 2 1
输出样例:
2
算法思路
完全二叉树(Complete Binary Tree)是二叉树的一种特殊形式,定义如下:
- 每一层都是满的:除了最后一层,其他每一层的节点数都达到最大。
- 最后一层节点从左到右排列:最后一层的节点必须连续排列,并且尽量向左填充。
简单来说,完全二叉树除了最后一层可能不满外,其他所有层都是完全填充的,并且最后一层的节点尽量靠左排列。
变量初始化:max 用来记录最大和,初始值设为 Integer.MIN_VALUE。res 用来记录最大和对应的层数,初始值为 0。
遍历每一层:使用变量 i 来表示每一层的起始节点,d 表示当前层数。每一层的节点数为 2^(d-1),即可以通过位运算 1 << (d - 1) 计算得到。对每一层,求出当前层的节点和。
更新最大值与层数:
如果当前层的节点和大于 max,则更新 max 和 ,同时记录此时的层数res = d;
代码如下
import java.io.*;
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 = 100010;
static int n;
static int[] arr = new int[N];
public static void main(String[] args) throws Exception{
n = nextInt();
for(int i = 1;i <= n;i++){
arr[i] = nextInt();
}
long max = Integer.MIN_VALUE;
int res = 0;
for(int i = 1,d = 1;i <= n;d++,i *=2){ //i表示每一层起点,d表示层数
long s = 0;
//2^(d-1)可以使用位运算即1左移d-1位即可
for(int j = i;j < i + (1 << (d - 1)) && j <= n;j++){
s += arr[j];
}
if(s > max){
max = s;
res = d;
}
}
pw.println(res);
pw.flush();
}
public static int nextInt()throws Exception{
st.nextToken();
return (int)st.nval;
}
}
总结
在解决《红与黑》、《交换瓶子》和《完全二叉树的权值》三道题时,我们使用了双指针、动态规划以及树形结构的认识。每一题的解法都涉及了对数据结构和算法的深入理解,挑战了我们在设计高效代码时的思维方式。通过这篇博客,大家不仅可以学到具体的代码实现,更能掌握解决类似问题的思路和方法。希望本篇博客能为你在算法的道路上提供帮助和启发。