dfs

1.1 排列组合问题

排列组合问题还得是卡子哥讲的通透

void backtracking(参数) {
    if (终止条件) {
        存放结果;
        return;
    }

    for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
        处理节点;
        backtracking(路径,选择列表); // 递归
        回溯,撤销处理结果
    }
}

 

组合与排列

 组合问题是从给定的元素集合中选取若干元素,不考虑元素的顺序,求子集的问题;而排列问题是从给定的元素集合中选取若干元素,考虑元素的顺序,求不同排列的问题。

组合问题: 假设有集合 {A, B, C},我们想要从中选择两个元素,不考虑顺序。组合问题涉及选择的元素集合,如 {A, B}、{A, C}、{B, C},而不考虑元素的排列顺序。例如,{A, B} 和 {B, A} 视为相同的组合。

排列问题: 同样假设有集合 {A, B, C},我们想要从中选择两个元素,考虑元素的排列顺序。排列问题涉及选择的元素排列,如 (A, B)、(A, C)、(B, A)、(B, C) 等,每个排列视为不同的选择,因为元素的顺序不同。

 1.1.1 组合问题

力扣 216.组合总和III

思路:回溯

终止条件:sum == n && path.size == k

循环条件:每种组合中不存在重复的数字 => i +1 

处理节点:sum+=i ; path.add(i)

递归:backTracking(i+1,sum,k)

剪枝:因为是[1,2,3...9]的递增有序集和,所以剪枝操作放到for循环判断更有效。剪枝判断显然是 sum > n 

回溯:sum-=i; path.remove(path.size()-1);

public class Solution {
    /*
     * //使用static关键字声明res和path作为类的静态成员会导致在算法执行过程中出现问题,
     * // 特别是当涉及到多个测试用例或多个Solution类的实例时。
     * static List<List<Integer>> res = new ArrayList<>();
     * static List<Integer> path = new LinkedList<>();
     */
    List<List<Integer>> res = new ArrayList<>();
    List<Integer> path = new LinkedList<>();

    public List<List<Integer>> combinationSum3(int k, int n) {
        backTracking(k, n, 1, 0);
        return res;
    }

    private void backTracking(int k, int n, int startIndex, int sum) {
        if (sum > n)
            return;

        if (path.size() > k)
            return;

        if (sum == n && path.size() == k) {
            res.add(new ArrayList<>(path));
            return;
        }

        for (int i = startIndex; i <= 9; i++) {
            path.add(i);
            sum += i;
            backTracking(k, n, i + 1, sum);
            sum -= i;
            path.remove(path.size() - 1);
        }
    }

}

组合总和II

与上一题区别在于:

  1. 本题数组candidates的元素是有重复的,而上一题是无重复元素的数组candidates

最后本题和上一题要求一样,解集不能包含重复的组合。上一题因为没有重复元素,只需要保证startIndex = i+1就不会出现重复组合

但是这题的 集合(数组candidates)有重复元素,但还不能有重复的组合。

对于这道问题,由于数组 candidates 中的元素可能重复,但解集中不能包含重复的组合,我们可以采用以下策略:

  1. 排序数组:首先对数组 candidates 进行排序,这样相同的元素会相邻排列。

  2. 回溯法避免重复:在回溯过程中,为了避免产生重复的组合,我们可以在每一层递归中跳过相同的元素,以确保不会重复选择相同的组合。这步操作对应卡哥说的树层去重操作.

 

class Solution {
  List<List<Integer>> res = new ArrayList<>();
  LinkedList<Integer> path = new LinkedList<>();
  int sum = 0;
  
  public List<List<Integer>> combinationSum2( int[] candidates, int target ) {
    //为了将重复的数字都放到一起,所以先进行排序
    Arrays.sort( candidates );
    backTracking( candidates, target, 0 );
    return res;
  }
  
  private void backTracking( int[] candidates, int target, int start ) {
    if ( sum == target ) {
      res.add( new ArrayList<>( path ) );
      return;
    }
    for ( int i = start; i < candidates.length && sum + candidates[i] <= target; i++ ) {
      //正确剔除重复解的办法
      //跳过同一树层使用过的元素
      if ( i > start && candidates[i] == candidates[i - 1] ) {
        continue;
      }

      sum += candidates[i];
      path.add( candidates[i] );
      // i+1 代表当前组内元素只选取一次
      backTracking( candidates, target, i + 1 );

      int temp = path.getLast();
      sum -= temp;
      path.removeLast();
    }
  }
}

  

1.1.2 排列问题

 

47.全排列 II

排列问题去重

class Solution {
    List<List<Integer>> res = new ArrayList<>();
    List<Integer> path = new ArrayList<>();
    boolean[] used;
    public List<List<Integer>> permuteUnique(int[] nums) {
        used = new boolean[nums.length];
        Arrays.fill(used,false);
        Arrays.sort(nums);
        backTracking(nums);
        return res;
    }
    public void backTracking(int[] nums){
        if(path.size() == nums.length){
            res.add(new ArrayList<>(path));
            return;
        }
        for(int i = 0 ; i < nums.length ; i++){
            if(i > 0 && nums[i-1] == nums[i] && used[i-1] == false){
                continue;
            }
            if(used[i] == false){
                used[i] = true;
                path.add(nums[i]);
                backTracking(nums);
                used[i] = false;
                path.removeLast();
            }
        }

    }
}

LeetCode 996. 正方形数组的数目

题解:全排列+排序去重+相邻元素特判
 
package com.coedes.dfs.likou996;


import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;

/**
 * @description:https://leetcode.cn/problems/number-of-squareful-arrays/
 * @author: wenLiu
 * @create: 2024/5/9 23:23
 */
public class Solution {

    int cnt = 0;

    public int numSquarefulPerms(int[] nums) {

        int n = nums.length;
        Arrays.sort(nums);
        boolean[] used = new boolean[n + 1];
        Arrays.fill(used, false);
        List<Integer> path = new LinkedList<>();
        dfs(0,nums, used, path);
        return cnt;
    }

    private void dfs(int i, int[] nums, boolean[] used, List<Integer> path) {
        if (i == nums.length) {
            cnt++;
            return;
        }
        for (int j = 0; j < nums.length; j++) {
            if (used[j]) continue;
            // 去重
            if (j > 0 && nums[j - 1] == nums[j] && !used[j-1]) continue;
            // 相邻元素和是否为完全平方数判断
            if (path.size() > 0 && !check(path.get(path.size() - 1) + nums[j])) continue;
            used[j] = true;
            path.add(nums[j]);
            dfs(i+1, nums, used, path);
            used[j] = false;
            path.remove(path.size() - 1);
        }
    }

    private boolean check(int i) {
        int sqrOfi = (int) Math.sqrt(i);
        return sqrOfi * sqrOfi == i;
    }
}

  

 

acwing 94. 递归实现排列型枚举

题解:通过dfs方式求排列,本质还是回溯

https://www.acwing.com/solution/content/44647/
import java.util.Scanner;

public class Main {
    static final int N = 10;
    static int[] nums = new int[N];  // 存储排列的数组
    static boolean[] st = new boolean[N];  // 标记数组,表示每个数字是否已经被使用过
    static int n;  // 排列的长度

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        n = scanner.nextInt();
        dfs(1);  // 从第一个位置开始深度优先搜索
    }

    static void dfs(int u) {
        // 如果已经排列完所有的数字,输出结果
        if (u > n) {
            for (int i = 1; i <= n; i++)
                System.out.print(nums[i] + " ");
            System.out.println();
            return;
        }
        // 枚举可选的数字
        for (int i = 1; i <= n; i++) {
            // 如果数字未被使用过,选择它并继续搜索
            if (!st[i]) {
                nums[u] = i;  // 将数字 i 放入当前位置
                st[i] = true;  // 标记数字 i 已被使用
                dfs(u + 1);  // 继续搜索下一个位置
                st[i] = false;  // 恢复状态,回溯
            }
        }
    }
}

  

2023柠檬微趣-排队

题目描述:

  1. 问题描述:给定了 m 位学生,每位学生有唯一的编号 1m。现在需要找出所有学生排队的方案,并按照字典序升序排序。

  2. 问题求解:需要找出所有可能的学生排队方案,这些方案按照字典序排序,然后输出第 n 个排队方案。

输入描述

输入第一行为一个整数 m ,表示学生和位置数。(1m10 )

输入第二行为一个整数 n ,表示排列方案序号。( 1n10^9 )

输出描述

若存在第 n 个方案,输出作为方案,数组间元素用空格隔开。

若不存在该方案,输出 −1

eg1:
input:
4
5

output:
1 4 2 3

eg2:
input:
3
7

output:
-1

 思路:m个数全排列+计数

  题目可以转化为 : 打印m个数字的全排列中的第n个排列

 这道题用卡哥

static List<List<Integer>> res = new ArrayList<>();

static List<Integer> path = new LinkedList<>();
收集答案会爆内存...
以后尽量用数组收集吧...

package com.coedes.dfs.ningmeng2023050604;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Arrays;



/**
 * @description:https://codefun2000.com/p/P1280
 * @author: wenLiu
 * @create: 2024/5/9 10:16
 */
public class Main {
    static int cnt = 0;
    public static void main(String[] args) throws IOException {
        BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
        int m = Integer.parseInt(reader.readLine());
        int n = Integer.parseInt(reader.readLine());

        int[] path = new int[m];

        boolean[] used = new boolean[m+1];
        Arrays.fill(used,false);
    
        int[] f = new int[m + 1];
        f[1] = 1;
        for (int i = 2; i <= m; i++) {
            f[i] = f[i - 1] * i;
        }
     //m个数全排列最多有m!种 
        if (f[m] < n) {
            System.out.println(-1);
        }else {
            dfs(0,m,n,path,used);
        }
    }

    private static void dfs(int i, int m, int n, int[] path, boolean[] used) {
        if (i == m) {
            cnt++;
            if (cnt==n) {
                for (int i1 : path) {
                    System.out.print(i1+" ");
                }
            }
            return;
        }
        for (int j = 1; j <= m; j++) {
            if (used[j]== true) {
                continue;
            }
            used[j] = true;
            path[i] = j;
            dfs(i+1,m,n,path,used);
            used[j] = false;
        }
    }
}

  

 1.2 子集方式枚举

 

子集方式枚举主要是考虑当前位置选还是不选,对应的复杂度一般为O(2^n),必须保证每个元素最多选取一次。
 

acwing 92. 递归实现指数型枚举

这是卡哥的写法,内存占用率比较大,之前碰到过内存溢出情况,但是比较好理解。

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

/**
 * @description:https://www.acwing.com/problem/content/description/94/
 * @author: wenLiu
 * @create: 2024/5/10 9:14
 */
public class Main {
    static List<List<Integer>> res = new ArrayList<>();
    static List<Integer> path = new LinkedList<>();

    public static void main(String[] args) throws IOException {
        BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
        int n = Integer.parseInt(reader.readLine());
        backTracking(1, n);
        for (List<Integer> list : res) {
            for (Integer i : list) {
                System.out.print(i + " ");
            }
            System.out.println();
        }
    }

    private static void backTracking(int start, int n) {
        res.add(new ArrayList<>(path));//特殊记忆 空集也要收集
        if (start > n) {
            //与组合问题唯一区别在于收集结果地方 一个是在叶子节点(组合),而当前子集问题是在每个节点上收集
//            res.add(new ArrayList<>(path));
            return;
        }
        for (int i = start; i <= n; i++) {
            path.add(i);// 子集收集元素
            backTracking(i+1,n);//start+1元素不重复取
            path.remove(path.size()-1);// 回溯
        }
    }
}

  

 这是dfs写法,优点在于消耗空间较小

import java.util.Scanner;

public class Main {
    static int N = 20;  // 定义常量N为20
    static int n;  // 定义变量n
    static boolean[] st = new boolean[N];  // 定义布尔数组st,长度为N

    public static void dfs(int u) {  // 定义深度优先搜索函数dfs
        if (u > n) {  // 如果u大于n,表示搜索结束
            for (int i = 1; i <= n; i++) {  // 遍历1到n
                if (st[i]) {  // 如果st[i]为true
                    System.out.print(i + " ");  // 输出i
                }
            }
            System.out.println();  // 输出换行
            return;  // 返回
        }
        st[u] = false;  // 将st[u]设为false
        dfs(u + 1);  // 递归调用dfs,表示不选当前元素
        st[u] = true;  // 将st[u]设为true
        dfs(u + 1);  // 递归调用dfs,表示选当前元素
    }

    public static void main(String[] args) {  // 主函数
        Scanner scanner = new Scanner(System.in);  // 创建一个Scanner对象
        n = scanner.nextInt();  // 读取n的值
        dfs(1);  // 调用dfs函数,从1开始搜索
    }
}

2022蚂蚁-数位和恰好为n的最大整数

题目描述:如何找到一个最大的正整数,使得该正整数所有数位之和等于给定的正整数 x ,且每个数位都不相等(任意一个数位不能是 00 )?

输入描述

一个正整数 x 。

1x100

输出描述

如果不存在合法解,请输出 −1 。

否则输出最大的满足条件的正整数

input:
9
output:
621

 

思路:子集方式枚举/二进制枚举

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;


/**
 * @description:https://codefun2000.com/p/P1017
 * @author: wenLiu
 * @create: 2024/5/10 9:50
 */
public class Main {
    static int[] w = new int[]{9, 8, 7, 6, 5, 4, 3, 2, 1};
    static String res = "";
    static final int N = 10;

    public static void main(String[] args) throws IOException {
        BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
        int x = Integer.parseInt(reader.readLine());

        //x <= 1+2+3....+9 = 9*(1+9)/2 = 45
        if (x > 45) {
            System.out.println(-1);
        } else {
            dfs(0, 0, x, "");
        }
        System.out.println(res);
    }

    private static void dfs(int u, int sum, int x, String s) {
        if (u == w.length) { // 判断递归终止条件,u 等于数组长度时结束递归
            if (sum == x) {
                if (res.length() < s.length() || (res.length() == s.length() && res.compareTo(s) < 0)) {
                    res = s;
                }
            }
            return;
        }

        // 递归调用 dfs,继续考虑下一个元素
        dfs(u + 1, sum + w[u], x, s + w[u]);
        dfs(u + 1, sum, x, s);
    }

}

 

题目描述:

公司有一些奖品,每个奖品有不同的价格,用正整数数组表示。公司希望将这些奖品分成三个大礼包,分别作为一等奖、二等奖和三等奖,使得:
- 一等奖的总价格  x  大于二等奖的总价格  y ,二等奖的总价格  y  大于三等奖的总价格  z 。
- 同时,一等奖和三等奖之间的价格差  x - z  最小。

现在需要找到一种合适的分配方案,使得满足上述条件。

输入描述

第一行:正整数 n ,表示奖品的数量,取值范围 [3,16)

第二行:一个正整数数组  array ,每个元素表示奖品的价格,取值范围 [1,1000]

输出描述

一个非负整数,表示一等奖和三等奖的差值,没有方案返回 0

input:
3
5 4 2
output:
3
 
题解:子集方式枚举/三进制枚举
考虑将第i个奖品分给 x , y , z 三人中的其中一个.
 
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class Main {
    static int res = Integer.MAX_VALUE;

    public static void main(String[] args) throws IOException {
        BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
        int n = Integer.parseInt(reader.readLine());
        int[] array = new int[n];
        String[] split = reader.readLine().split(" ");
        long total = 0;
        for (int i = 0; i < n; i++) {
            array[i] = Integer.parseInt(split[i]);
            total += array[i];
        }

        int[] choose = new int[3];
        dfs(0, total, array, choose);
        System.out.println(res);
    }

    private static void dfs(int u, long total, int[] array, int[] choose) {
        if (u == array.length) {
            if (total == choose[0] + choose[1] + choose[2]
                    && choose[0] > choose[1]
                    && choose[1] > choose[2]) {
                res = Math.min(res, choose[0] - choose[2]);
            }
            return;
        }
        for (int i = 0; i < choose.length; i++) {
            choose[i] += array[u];
            dfs(u + 1, total, array, choose);
            choose[i] -= array[u];
        }
    }
}

  

2023阿里-数组权值

题目描述:

 

给定一个长度为 n  的数组,定义该数组的“数组权值”为其每个元素的“元素权值”之和。

对于数组中的第 i 个元素,其“元素权值”定义为在包含该元素的所有子集中,子集元素之和大于等于 0 的子集数量。

你的任务是计算并输出这个数组的“数组权值”。

 

这样描述更为简洁明了,便于理解题意。

输入描述

第一行,一个正整数 (1n18) ,表示数组的长度。

第二行,n个整数表示数组 a ,第 i 个元素为ai​ (109≤ai109)

数据保证每个 ai 都是不同的。

 

思路:子集方式枚举
 
二进制枚举: 数组长度为n,选或者不选当前元素i , 因此子集数量为2n (n = 18), n < 20 可以接受。
  1. 枚举所有子集 
  2. 计算每个子集元素和
  3. 统计包含每个元素的子集数量
eg1:
3
-1 2 3

子集 000 (空子集):和 = 0,元素数量 = 0,不计算在内。
子集 001:包含元素 3,和 = 3,元素数量 = 1,总权值增加 1。
子集 010:包含元素 2,和 = 2,元素数量 = 1,总权值增加 1。
子集 011:包含元素 2, 3,和 = 5,元素数量 = 2,总权值增加 2。
子集 100:包含元素 -1,和 = -1,不计算在内。
子集 101:包含元素 -1, 3,和 = 2,元素数量 = 2,总权值增加 2。
子集 110:包含元素 -1, 2,和 = 1,元素数量 = 2,总权值增加 2。
子集 111:包含元素 -1, 2, 3,和 = 4,元素数量 = 3,总权值增加 3。
总权值计算 子集和大于等于0的元素数量之和: 1 (子集 001) + 1 (子集 010) + 2 (子集 011) + 2 (子集 101) + 2 (子集 110) + 3 (子集 111) = 11

  

import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);

        int n = scanner.nextInt(); // 读取数组的长度

        int[] a = new int[n];
        for (int i = 0; i < n; ++i) {
            a[i] = scanner.nextInt(); // 读取数组元素
        }

        int totalWeight = 0; // 初始化总权值
        int subsetCount = 1 << n; // 子集的数量为 2^n

        // 枚举所有子集
        for (int i = 0; i < subsetCount; ++i) {
            long sum = 0;
            int count = 0;
            // 计算子集的和
            for (int j = 0; j < n; ++j) {
                if ((i & (1 << j)) != 0) { // 判断子集是否包含第 j 个元素 
                    sum += a[j];
                    count += 1;
                }
            }
            // 如果子集的和大于等于 0,则增加总权值
            if (sum >= 0) {
                totalWeight += count;
            }
        }

        System.out.println(totalWeight); // 输出总权值
    }
}

 

 

题目描述

 

在一个限行城市中,每天一些车牌号的最后一位数字被禁止上路。需要确保每天都有车可以开。假设不能换车牌号,也不能选择其他交通方式,问至少需要买多少辆车?如果无法做到,请输出-1。

 

输入描述

 

输入共 7 行,表示周一至周日的限行情况。

每行的第一个数字 c 表示当天限行数字的个数,接着输入ci 个互不相同的数字,第 j 个数字为 aij,表示限行数字。

对于所有的数据,0<= ci <= 10, 0 <= aij <=  9.。

 

输出描述

 

输出一个整数,表示需要的最少车辆数或无法保证每天至少有一辆车可以出行时输出 -1。

 

思路:位运算+子集枚举

解题思路

1. 输入处理:

- 读取输入的限行尾号信息。

- 使用位掩码表示每一天的限行尾号情况。

 

2. 位掩码表示:

- 将每一天限行的尾号通过二进制位记录下来。例如,尾号1、3、5限行,表示为二进制`0000 1010 10 `。

 

3. 计算不限行尾号:

- 通过二进制`1111 1111 11`表示所有尾号(0-9)。

- 每天的不限行尾号为 `1111111111` 减去限行的尾号。

 

4. 枚举所有尾号组合:

- 枚举所有可能的尾号组合,范围是从 `1` 到 `1111111111` (二进制表示)。

 

5. 合法性判断:

- 检查当前组合是否在每一天都有合法的尾号。如果某一天无交集,则认为当前组合不合法。

- 如果合法,记录当前组合中最少的尾号数量。

 

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Scanner;

/**
 * @description:https://codefun2000.com/p/P1267
 * @author: wenLiu
 * @create: 2024/5/14 14:28
 */
public class Main {
    public static void main(String[] args) throws IOException {
        BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));

        int[] bit = new int[7]; // 分别存储七天中不限行的号码

        for (int i = 0; i < 7; i++) {
            //1. 输入处理:
            String[] str = reader.readLine().split(" ");
            int c = Integer.parseInt(str[0]);
            for (int j = 0; j < c; j++) {
                //2. 位掩码表示:
                bit[i] |= 1 << Integer.parseInt(str[j + 1]);
            }
            //3. 计算不限行尾号:
            bit[i] = Integer.parseInt("1111111111", 2) - bit[i];
        }

        long res = -1;

        //4. 枚举所有尾号组合:(1~1111 1111 11)
        for (int i = 1; i <= Integer.parseInt("1111111111", 2); i++) {
            int cnt = 0;
            for (int j = 0; j < 10; j++) {
                if (((i >> j) & 1) != 0) {
                    cnt++; // 当前组合汽车数量
                }
            }

            //5. 合法性判断:
            boolean ok = true;
            for (int j = 0; j < 7; j++) {
                if ((bit[j] & i) == 0) {
                    ok = false;
                    break;
                }
            }

            // 如果合法,取最少的尾号数量
            if (ok) {
                if (res == -1) res = cnt; // 初始赋值
                else res = Math.min(res, cnt); // 更新最少数量
            }
        }

        System.out.println(res);
    }
}

  

 

1.3 暴搜题

   一般这种题型的数据范围为1 <= n <= 20,可以考虑使用DFS枚举所有情况,然后去找到满足条件的最优解方案数。

 

2023腾讯-重组字符串

 

题目描述:给定 N 个小写字母字符串,每个字符串最长为 8 个字母。每次从每个字符串中选出一个字母,拼成一个新的字符串,要求新字符串中不能有重复的字母。计算可以拼出的不同重组字符串的数量。

输入描述

  • 第一行输入一个整数 N (2 < N < 6)。
  • 接下来 N 行,每行一个由小写字母组成的字符串,长度在 1 到 7 之间。

输出描述

  • 输出一个整数,表示可以拼出的不同重组字符串的数量。

 

input:
5
abca
acds
aca
vac
bba
output:
6

  

 思路: 回溯 + 哈希去重

import java.util.*;

public class Main {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int n = sc.nextInt();
        String[] strings = new String[n];
        for (int i = 0; i < n; i++) {
            strings[i] = sc.next();
        }
        Set<String> resultSet = new HashSet<>();
        backtrack(strings, 0, new StringBuilder(), new boolean[26], resultSet);
        System.out.println(resultSet.size());
    }

    private static void backtrack(String[] strings, int index, StringBuilder current, boolean[] used, Set<String> resultSet) {
        if (index == strings.length) {
            resultSet.add(current.toString());
            return;
        }
        for (char c : strings[index].toCharArray()) {
            if (!used[c - 'a']) {
                used[c - 'a'] = true;
                current.append(c);
                backtrack(strings, index + 1, current, used, resultSet);
                current.deleteCharAt(current.length() - 1);
                used[c - 'a'] = false;
            }
        }
    }
}

 

思路:dfs + 内部去重(剪枝)+ 集和去重

N 个小写字母字符串,每个字符串最长为 8 个字母,说明递归深度为N,每层递归的选择数为8,则O(N) = 8 N

内部去重:

 

  • 在递归过程中,每层递归都需要处理当前字符串的每个字符。为了减少重复计算和递归调用次数,内部去重是必要的。
  • 如果一个字符串中有重复字符,这个逻辑可以避免对这些重复字符进行多余的递归调用,从而减少递归树的大小,节省时间。
  • 例如,考虑一个字符串 "aabb" 排序后是 "aabb"。如果不进行内部去重,每次递归都将处理两个 'a' 和两个 'b',导致大量重复的计算。

 

集合去重:

 

  • 集合去重的目的是在递归完成并生成一个候选字符串后,确保这个字符串中的所有字符都是唯一的。
  • 它是在递归的最深层进行检查,用于验证最终生成的字符串是否符合题目要求(没有重复字符)。

 

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Arrays;
import java.util.HashSet;

/**
 * @description:https://codefun2000.com/p/P1123
 * @author: wenLiu
 * @create: 2024/5/15 10:13
 */
public class Main {
    static int res;

    public static void main(String[] args) throws IOException {
        BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
        int N = Integer.parseInt(reader.readLine());
        String[] strings = new String[N];
        for (int i = 0; i < N; i++) {
            char[] chars = reader.readLine().toCharArray();
            Arrays.sort(chars);
            strings[i] = new String(chars);
        }

        dfs(0, N, new StringBuffer(), strings);
        System.out.println(res);
    }

    private static void dfs(int u, int N, StringBuffer s, String[] strings) {
        if (u == N) {
            HashSet<Character> set = new HashSet<>();
            for (char c : s.toString().toCharArray()) {
                set.add(c);
            }
            if (set.size() == N) {
                res++;
            }
            return;
        }

        char[] chars = strings[u].toCharArray();
        for (int i = 0; i < chars.length; i++) {
            s.append(chars[i]);
            dfs(u + 1, N, s, strings);
            s.deleteCharAt(s.length() - 1);
            while (i + 1 < chars.length && chars[i] == chars[i + 1]) {
                i++;
            }
        }
    }
}

  

题目描述

将 N 个小玩具分装到 M 个小包装内,每个小包装的总价值为其内玩具数量的平方。找到一种分装方案,使得所有小包装的价值之和恰好为 P,并且字典序最小。

输入描述

  • 第一行:三个整数 N、M 和 P,分别表示玩具数量、小包装数量和总价值。
  • 1 ≤ N ≤  M ≤ 12
  • 0 ≤ P ≤ 109

输出描述

  • 输出一个整数数组,表示符合条件的分装方案。如果不存在这样的方案,输出 -1。

 

input:
4 3 6

output:
1 1 2

 

思路:

解题思路

  1. 暴力搜索:

    • 从小到大枚举所有可能的情况。
    • 因为从小到大枚举,第一次搜索到的结果一定是字典序最小的。
  2. 剪枝优化:

    • 定义搜索的上下边界。
    • 下边界:1。
    • 上边界:根据当前已搜到第 i 个数,总和为 total,平方和为 sum,剩下 m-i 个数,最极端的情况是这 m-i-1 个数(i从0开始填 1,剩余的这个数就是最大能枚举到的数。
      • 对于 n 这个限制,最大能枚举到的数取值为 n - total - (m - i - 1)
      • 对于 p 这个限制,最大能枚举到的数取值为 sqrt(p - sum - (m - i - 1))
  3. 更新上下边界:

    • 上边界取上述两个值的最小值,确保既满足总和 n 的限制,也满足平方和 p 的限制。
 
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

/**
 * @description:from https://codefun2000.com/p/P1074
 * @author: wenLiu
 * @create: 2024/5/16 9:45
 */

public class Main {
    static String res = "";

    public static void main(String[] args) throws IOException {
        BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
        String[] split = reader.readLine().split(" ");
        int N = Integer.parseInt(split[0]);
        int M = Integer.parseInt(split[1]);
        int P = Integer.parseInt(split[2]);

        StringBuffer current = new StringBuffer();
        int initialUp = Math.min(N - (M - 1), (int) Math.sqrt(P));
        dfs(0, initialUp, M, 0, N, 0, P, current);

        if (res.length() == 0) System.out.println("-1");
        else {
            for (int i = 0; i < res.length(); i++) {
                if (i != 0) System.out.print(" ");
                System.out.print(res.charAt(i));
            }
        }
    }

    private static void dfs(int u, int up, int m, int cnt, int n, int sum, int p, StringBuffer s) {
        if (u > m || sum > p) return; // 剪枝
        if (u == m) {
            if (sum == p && cnt == n) {
                String resultString = s.toString();
                if (res.length() == 0 || resultString.compareTo(res) < 0) {
                    res = resultString;
                }
            }
            return;
        } // 收集结果

        // 枚举玩具个数
        for (int i = 1; i <= up; i++) {
            s.append(i);
            int newUp = Math.min((int) Math.sqrt(p - sum - (m - u - 1)), n - cnt - (m - u - 1));// 剪枝
            dfs(u + 1, newUp, m, cnt + i, n, sum + (i * i), p, s);
            s.deleteCharAt(s.length() - 1);
        }
    }
}

 

acwing 5165. CCC单词搜索

题解:

本题是有八个方向的(水平两个方向,竖直两个方向,斜45度四个方向)由于题目说明可以转向,但是只能转一次,因此可以多定义一个变量 flag 表示是否转向,

注意在DFS的时候我们开始会定义一个起始方向,因此只能在至少走了一步之后才可以转弯(转弯有顺时针和逆时针两个方向)(表示顺时针转弯),

设当前定义的方向为 dirc ,转弯后的方向可以变为

(dirc+1)%4(表示逆时针转弯)

(dirc+3)%4 (表示顺时针转弯)

 

  • dx[0], dy[0] -> 右 (East)
  • dx[1], dy[1] -> 下 (South)
  • dx[2], dy[2] -> 左 (West)
  • dx[3], dy[3] -> 上 (North)
  • dx[4], dy[4] -> 右下 (Southeast)
  • dx[5], dy[5] -> 右上 (Northeast)
  • dx[6], dy[6] -> 左上 (Northwest)
  • dx[7], dy[7] -> 左下 (Southwest)

 

import java.util.Scanner;

public class Main {
    static char[][] g; // 字母矩阵
    static String s; // 目标单词
    static int n, m, k, res; // 矩阵的行数、列数,单词长度,结果计数
    static int[] dx = {1, 0, -1, 0, 1, 1, -1, -1}; // 8个方向的x增量
    static int[] dy = {0, 1, 0, -1, -1, 1, 1, -1}; // 8个方向的y增量

    // 深度优先搜索
    public static void dfs(int x, int y, int u, int dirc, int flag) {
        // 越界或字符不匹配,返回
        if (x < 0 || x >= n || y < 0 || y >= m || g[x][y] != s.charAt(u)) {
            return;
        }
        // 已匹配到单词的最后一个字符
        if (u == k - 1) {
            res++;
            return;
        }
        // 继续向当前方向搜索
        int a = x + dx[dirc];
        int b = y + dy[dirc];
        dfs(a, b, u + 1, dirc, flag);
        // 处理转角情况
        if (flag == 0 && u > 0) {
            int t = 0;
            if (dirc >= 4) {
                t = 4; // 如果当前方向在斜向上,调整到斜向下
                dirc -= 4;
            }
            // 转90度后的两个新方向
            int d1 = t + (dirc + 3) % 4;
            int d2 = t + (dirc + 1) % 4;
            // 新方向上的新位置
            int a1 = x + dx[d1];
            int b1 = y + dy[d1];
            int a2 = x + dx[d2];
            int b2 = y + dy[d2];
            // 搜索新方向
            dfs(a1, b1, u + 1, d1, flag + 1);
            dfs(a2, b2, u + 1, d2, flag + 1);
        }
    }

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        s = scanner.next(); // 读取目标单词
        k = s.length(); // 单词长度
        n = scanner.nextInt(); // 矩阵行数
        m = scanner.nextInt(); // 矩阵列数
        g = new char[n][m]; // 初始化矩阵

        // 读取矩阵
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                g[i][j] = scanner.next().charAt(0);
            }
        }

        res = 0; // 初始化结果计数
        // 遍历矩阵的每一个位置
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                // 从当前字符的8个方向开始搜索
                for (int d = 0; d < 8; d++) {
                    dfs(i, j, 0, d, 0);
                }
            }
        }
        // 输出结果
        System.out.println(res);
    }
}

 

2023华为od-上班之路

题目描述:

 

给定一个由以下元素组成的地图:
- ”.” — 空地,可以通过;
- ”*” — 路障,不可通过;
- ”S” — 起点;
- ”T” — 终点。

在限制拐弯次数和可以清除一定数量路障的条件下,计算是否可以从起点到达终点。

输入描述:

第一行输入拐弯次数和清理个数。
第二行输入n,m代表地图大小 (1<=n,m<=100)。
接下来是n*m大小的矩阵,表示地图。

输出描述:

输出是否可以从起点到达终点,是则输出YES,否则输出NO。

input:
2 0
5 5
..S..
****.
T....
****.
.....
output:
YES

 

思路:DFS+转弯特判

  

题解简化:

  • 这是一个经典的DFS问题,用于判断起点能否到达终点。
  • 本题有两个限制条件:拐弯次数和清除障碍物的次数。
    •  对于拐弯次数:需要知道上一次行走的方向,并与当前行走方向对比。如果不同,则拐弯次数+1;相同则不变。
    •  技巧:初始方向设为-1(上下左右方向分别为0~3),初始拐弯次数设为-1(第一次走时不增加拐弯次数)。
  • 根据上述条件,使用DFS进行暴力搜索(注意回溯)。
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class Main {
    private static int k1, k2; // 最大允许拐弯次数和最大允许清除障碍次数
    private static int n, m; // 地图的行数和列数
    private static char[][] g; // 地图矩阵
    private static boolean[][] st; // 标记矩阵,用于标记是否访问过

    // 定义上下左右四个方向
    private static int[] dx = {1, 0, -1, 0};
    private static int[] dy = {0, -1, 0, 1};

    public static void main(String[] args) throws IOException {
        BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
        // 读取最大拐弯次数和最大清除障碍次数
        String[] s = reader.readLine().split(" ");
        k1 = Integer.parseInt(s[0]);
        k2 = Integer.parseInt(s[1]);

        // 读取地图的行数和列数
        s = reader.readLine().split(" ");
        n = Integer.parseInt(s[0]);
        m = Integer.parseInt(s[1]);

        // 初始化地图矩阵和标记矩阵
        g = new char[n][m];
        st = new boolean[n][m];

        // 读取地图矩阵
        for (int i = 0; i < n; i++) {
            g[i] = reader.readLine().toCharArray();
        }

        // 寻找起点,并从起点开始DFS
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                if (g[i][j] == 'S') {
                    st[i][j] = true; // 标记起点为已访问
                    if (dfs(i, j, -1, 0, 0)) {
                        System.out.println("YES");
                    } else {
                        System.out.println("NO");
                    }
                    return; // 找到起点后不再继续搜索
                }
            }
        }
    }

    /**
     * 深度优先搜索函数
     * @param u 当前x坐标
     * @param v 当前y坐标
     * @param dic 上一次移动的方向
     * @param w1 当前的拐弯次数
     * @param w2 当前清除障碍的次数
     * @return 是否能到达终点
     */
    private static boolean dfs(int u, int v, int dic, int w1, int w2) {
        // 如果到达终点且拐弯次数和清除障碍次数均不超过限制,返回true
        if (g[u][v] == 'T' && w1 <= k1 && w2 <= k2) return true;

        // 遍历四个方向
        for (int i = 0; i < 4; i++) {
            int nextX = u + dx[i];
            int nextY = v + dy[i];

            // 检查边界条件和是否访问过
            if (nextX < 0 || nextX >= n || nextY < 0 || nextY >= m || st[nextX][nextY]) continue;

            // 更新拐弯次数和清除障碍次数
            int newW1 = w1 + (dic != i && dic != -1 ? 1 : 0);
            int newW2 = w2 + (g[nextX][nextY] == '*' ? 1 : 0);

            // 检查是否超过限制
            if (newW1 > k1 || newW2 > k2) continue;

            // 标记当前位置为已访问
            st[nextX][nextY] = true;
            // 递归调用DFS,如果能到达终点,返回true
            if (dfs(nextX, nextY, i, newW1, newW2)) return true;
            // 回溯,取消标记
            st[nextX][nextY] = false;
        }
        return false; // 如果所有方向都不能到达终点,返回false
    }
}

  

1.4 连通块问题

这是连通块模板题...

题目描述:

给定一个由 '1'(陆地)和 '0'(水)组成的二维网格,计算岛屿的数量。岛屿由水平或垂直相邻的陆地连接形成。网格边缘全是水。

 

eg1:
input:
grid = [
  ["1","1","1","1","0"],
  ["1","1","0","1","0"],
  ["1","1","0","0","0"],
  ["0","0","0","0","0"]
]
output:
1

eg2:
input:
grid = [
  ["1","1","0","0","0"],
  ["1","1","0","0","0"],
  ["0","0","1","0","0"],
  ["0","0","0","1","1"]
]
output:
3

 

思路:从左到右,下从上到下遍历所有值为‘1’的起点,从该起点进行dfs,走过的节点st置为true,dfs结束岛屿个数累加即可。

具体来说就是:

  1. 初始化岛屿计数器为 0。
  2. 遍历网格的每个节点:
    • 如果节点值为 '1',启动 DFS 并将岛屿计数器加一。
  3. 在 DFS 函数中:
    • 检查当前节点是否越界或是否已访问(值为 '0')。
    • 标记当前节点为已访问(置为 '0')。
    • 递归调用 DFS 处理四个方向(上、下、左、右)。

 

package com.coedes.dfs.likou200;

/**
 * @description:from https://leetcode.cn/problems/number-of-islands/
 * @author: wenLiu
 * @create: 2024/5/18 11:00
 */
public class Solution {
    int n, m;
    boolean[][] st;

    int[] dx = {1, 0, -1, 0};
    int[] dy = {0, -1, 0, 1};

    public int numIslands(char[][] grid) {
        n = grid.length;
        m = grid[0].length;
        st = new boolean[n][m];

        int res = 0;
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                if (!st[i][j] && grid[i][j] == '1') {
                    dfs(i, j, grid);
                    res++;
                }
            }
        }
        return res;
    }

    private void dfs(int i, int j, char[][] grid) {
        st[i][j] = true;
        for (int d = 0; d < 4; d++) {
            int nextX = i + dx[d];
            int nextY = j + dy[d];
            if (nextX < 0 || nextX >= n || nextY < 0 || nextY >= m) continue;
            if (!st[nextX][nextY] && grid[nextX][nextY] == '1') {
                dfs(nextX, nextY, grid);
            }
        }
    }
}

 

题目描述:

  • 给定一个由 'R'、'G'、'B' 组成的矩阵,计算蓝绿色盲视角中和真实情况的连通块数量差。
  • 连通块指的是上下左右相连的相同颜色区域。

输入描述:

  • 第一行:两个正整数 n 和 m,表示矩阵的行数和列数。
  • 接下来 n 行:每行一个长度为 m 的字符串,表示矩阵。(1n,m1000

 

输出描述:

  • 一个整数,表示蓝绿色盲视角比真实情况少的连通块数量。
input:
2 6
RRGGBB
RGBGRR
output: 3

  

思路:

  1. 初始化:读取矩阵,并初始化计数器。
  2. 真实情况计数:遍历矩阵,使用 DFS 或 BFS 计算真实的连通块数量。
  3. 蓝绿色盲视角计数:将 'G' 和 'B' 视为一种颜色,重新计算连通块数量。(注意要重新初始化st数组)
  4. 计算差值:输出真实情况和蓝绿色盲视角的连通块数量差值。

 

package com.coedes.dfs.mhy2023031901;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;


/**
 * @description:from https://codefun2000.com/p/P1094
 * @author: wenLiu
 * @create: 2024/5/18 13:24
 */
public class Main {
    // 矩阵的行数和列数
    static int n, m;
    // 矩阵表示的网格
    static char[][] grid;
    // 标记矩阵,用于DFS搜索时标记已访问的节点
    static boolean[][] st;

    // 四个方向:左、下、右、上
    static int[] dx = {1, 0, -1, 0};
    static int[] dy = {0, -1, 0, 1};

    public static void main(String[] args) throws IOException {
        BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
        // 读取输入的行数和列数
        String[] split = reader.readLine().split(" ");
        n = Integer.parseInt(split[0]);
        m = Integer.parseInt(split[1]);
        grid = new char[n][m];
        st = new boolean[n][m];

        // 读取网格数据
        for (int i = 0; i < n; i++) {
            grid[i] = reader.readLine().toCharArray();
        }

        // 计算真实情况的连通块数量
        int cnt1 = getCnt();

        // 重置标记数组
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                st[i][j] = false;
            }
        }

        // 将所有的 'B' 替换为 'G',模拟蓝绿色盲视角
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                if (grid[i][j] == 'B') {
                    grid[i][j] = 'G';
                }
            }
        }

        // 计算蓝绿色盲视角的连通块数量
        int cnt2 = getCnt();

        // 输出两个视角连通块数量的差值
        System.out.println(cnt1 - cnt2);
    }

    // 计算连通块的数量
    private static int getCnt() {
        int cnt = 0;
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                // 如果该位置没有被访问过,则进行DFS,并增加连通块计数
                if (!st[i][j]) {
                    dfs(i, j, grid[i][j]);
                    cnt++;
                }
            }
        }
        return cnt;
    }

    // 深度优先搜索(DFS)用于遍历连通块
    private static void dfs(int i, int j, char c) {
        // 标记当前节点为已访问
        st[i][j] = true;
        // 遍历四个方向
        for (int d = 0; d < 4; d++) {
            int nextX = i + dx[d];
            int nextY = j + dy[d];
            // 边界检查,防止数组越界
            if (nextX < 0 || nextX >= n || nextY < 0 || nextY >= m) continue;
            // 如果下一个节点未被访问过且与当前节点颜色相同,则继续DFS
            if (!st[nextX][nextY] && grid[nextX][nextY] == c) {
                dfs(nextX, nextY, c);
            }
        }
    }
}

 

2023050302 华为od

 题目描述:给定一个 N 行 M 列的农田,每个格子有两种状态:0 表示感染病毒,1 表示未感染病毒。一次操作只能感染一个未感染植物,并使其相邻的八个方向上的植物也被感染。问需要多少次操作才能使所有植物都感染病毒。

 

输入描述

  • 第一行输入两个整数 N 和 M(1 ≤ N, M ≤ 1000),分别表示矩阵的行数和列数。
  • 接下来输入 N 行,每行 M 个数字,表示矩阵的状态,数字用空格隔开。

输出描述

  • 输出使矩阵中所有植物均感染所需的最少操作次数。

示例

 输入:

3 3
1 0 1
0 1 0
1 0 1

 

输出

1

  

思路

  1. 遍历矩阵,用 DFS 或 BFS 查找并感染所有相连的未感染区域。
  2. 记录每次操作感染的区域数。
  3. 输出操作次数。

这种问题本质上是求连通分量的个数,每个连通分量代表一次操作所能感染的所有区域。

 

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.StringTokenizer;

public class Main {
    static int N, M;
    static int[][] grid;
    static boolean[][] visited;

    // 定义八个方向
    static int[] dx = {1, 1, 1, 0, 0, -1, -1, -1};
    static int[] dy = {0, 1, -1, 1, -1, 0, 1, -1};

    public static void main(String[] args) throws IOException {
        BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer tokenizer = new StringTokenizer(reader.readLine());
        N = Integer.parseInt(tokenizer.nextToken());
        M = Integer.parseInt(tokenizer.nextToken());
        
        grid = new int[N][M];
        visited = new boolean[N][M];
        
        for (int i = 0; i < N; i++) {
            tokenizer = new StringTokenizer(reader.readLine());
            for (int j = 0; j < M; j++) {
                grid[i][j] = Integer.parseInt(tokenizer.nextToken());
            }
        }

        int operations = 0;

        // 遍历所有格子,寻找未感染区域,并进行DFS感染
        for (int i = 0; i < N; i++) {
            for (int j = 0; j < M; j++) {
                if (grid[i][j] == 1 && !visited[i][j]) {
                    dfs(i, j);
                    operations++;
                }
            }
        }

        System.out.println(operations);
    }

    // 深度优先搜索函数,用于感染相邻的未感染植物
    private static void dfs(int x, int y) {
        visited[x][y] = true;

        for (int d = 0; d < 8; d++) {
            int nx = x + dx[d];
            int ny = y + dy[d];

            if (nx >= 0 && nx < N && ny >= 0 && ny < M && grid[nx][ny] == 1 && !visited[nx][ny]) {
                dfs(nx, ny);
            }
        }
    }
}

  

 2023081204美团

 

题目简化

给定一个长度为 n 的字符串。可以将其转换成大小为 x*y 的矩形,保证 x*y = n。

矩形的权值定义为这个矩形的连通块数目。上下左右相邻的相同字符将被视为同一个连通块。

要求在所有可能的矩形中找到权值最小的矩形,并输出其权值。

输入描述

第一行输入一个整数 n,代表字符串的长度。(1n104
第二行输入一个长度为 n 的仅由小写字母组成的字符串。

输出描述

输出一个整数表示最小权值。

 

eg:

input:
8
abaababa

output:
3

 

思路::枚举 + DFS/BFS 求连通块个数

1. 因子计算:
  对 10000 以内的数进行因子枚举。

2. 坐标映射(二维数组映射到一维数组,408好像经常考,死去的记忆向我袭来,haha...):
  将字符串按行列进行映射,设字符串下标为  i ,对应矩阵坐标为 (i / m, i % m)。

3. 计算连通块:
  使用 DFS/BFS 在矩阵中查找连通块个数。

 

import java.util.*;

public class Main {
    static int n, m, k; // 矩阵的行数,列数,以及字符串的长度
    static int[] dx = {-1, 1, 0, 0}, dy = {0, 0, 1, -1}; // 定义四个方向的移动数组,上下左右
    static String s; // 输入的字符串
    static boolean[][] st; // 标记矩阵,用于记录某位置是否已访问过

    // 深度优先搜索 (DFS) 方法,用于遍历连通块
    static void dfs(int x, int y) {
        st[x][y] = true; // 标记当前位置为已访问
        for (int i = 0; i < 4; i++) { // 遍历四个方向
            int nextX = dx[i] + x; // 计算新位置的行坐标
            int nextY = dy[i] + y; // 计算新位置的列坐标
            // 检查边界条件,是否已经访问过,以及字符是否相同
            if (nextX < 0 || nextX >= n || nextY < 0 || nextY >= m || st[nextX][nextY] || s.charAt(nextX * m + nextY) != s.charAt(x * m + y))
                continue;
            dfs(nextX, nextY); // 递归调用DFS
        }
    }

    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        k = sc.nextInt(); // 读取字符串长度
        s = sc.next(); // 读取字符串
        int res = k; // 初始化结果为字符串长度
        
        // 枚举所有可能的矩阵尺寸
        for (int x = 1; x <= k; x++) {
            if (k % x != 0) continue; // 如果不能整除,跳过
            n = x; // 设置行数
            m = k / n; // 设置列数
            st = new boolean[n][m]; // 初始化标记矩阵为未访问
            
            int cnt = 0; // 连通块计数器
            // 遍历矩阵中的每个位置
            for (int i = 0; i < n; i++) {
                for (int j = 0; j < m; j++) {
                    if (!st[i][j]) { // 如果该位置未被访问
                        cnt++; // 连通块数量加1
                        dfs(i, j); // 执行DFS遍历整个连通块
                    }
                }
            }
            res = Math.min(res, cnt); // 更新结果为最小连通块数量
        }
        System.out.println(res); // 输出结果
    }
}

  

华为od2023042002 

  题目描述

给定一个生成对称字符串的规则,从初始字符串 `R` 开始:
1. 第一个对称字符串是 `B`
2. 然后取反得 `R`
3. 再加上原来的字符串 `BR`
4. 继续取反和拼接得到 `RBBR`

这样可以生成一系列对称字符串。

每个字符的位置编号从左到右依次为 `0, 1, 2, ...`。找出第 n 个对称字符串中第 k 个字符的具体值 ( k 的编号从 00 开始)。

 输入描述

第一行输入一个整数 `T`,表示有 `T` 组用例。
每个用例的第一行为两个整数 `n, k`。
- `1 ≤ T ≤ 100`
- `1 ≤ n ≤ 64`
- `0 ≤ k < 2^n - 1`

 输出描述

输出为 `T` 行,每一行输出 `blue` 表示字符为 `B`,`red` 表示字符为 `R`。

示例

输入:
5
4 2
2 0
1 0
3 2
5 12

输出:
red
blue
red
blue
red

  

思路:二叉树递归

可将整个字符串生成过程看成是一棵二叉树:

  • 'R' 用 1 表示
  • 'B' 用 0 表示

设根节点为 0('R'),则:

  • 左儿子为 1 ('B')
  • 右儿子为 0 ('R')

设 `F(i, j)` 为第 `i` 层第 `j` 个节点的颜色:

  • `F(i, j)` 的父节点是 `F(i-1, j//2)`
  •  if(j%2==1)(右儿子),颜色与父节点相同  return F(i-1,j/2);
  •  if(j%2==0)(左儿子),颜色与父节点相同  return !F(i-1,j/2);

通过递归从根节点推导到目标节点,确定目标节点的颜色。

import java.util.Scanner;

/**
 * @description:from https://codefun2000.com/p/P1240
 * @author: wenLiu
 * @create: 2024/5/19 9:33
 */
public class Main {
    static long T, n, k;  // T是测试用例的数量,n和k是每个用例中的两个输入参数

    // 递归函数,计算n层k位置节点的颜色值
    static int get_val(long n, long k) {
        if (n == 0) return 0;  // 基本情况,根节点的颜色为0(对应blue)
        if ((k & 1) == 1) return get_val(n - 1, k / 2);  // 如果k是奇数(右儿子),颜色与父节点相反
        return 1 - get_val(n - 1, k / 2);  // 如果k是偶数(左儿子),颜色与父节点相同
    }

    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        T = sc.nextLong();  // 读取测试用例的数量
        while (T-- > 0) {
            n = sc.nextLong();  // 读取每个用例中的参数n
            k = sc.nextLong();  // 读取每个用例中的参数k
            // 根据get_val函数的返回值,输出相应的颜色
            if (get_val(n, k) == 1) System.out.println("red");
            else System.out.println("blue");
        }
    }
}

 

acwing 3695.扩充序列

题目描述:

给定一个初始序列 [1],经过 n-1 次扩充后,每次扩充规则如下:

1. 将序列本身添加到序列的尾部。
2. 在两个相同的序列中间插入一个还未使用过的最小正整数。

 

例如:

初始序列:[1]
第一次扩充:[1, 2, 1]
第二次扩充:[1, 2, 1, 3, 1, 2, 1]

现在,请计算在经过 n-1 次扩充后,序列的第 k 个元素的值是多少?(序列从 1 开始编号)(第一感觉从这里就能推测应该用dp做...)

 

输入描述:

  • 第一行一个整数 T,表示有 T 组测试用例。
  • 每组测试用例包含两个整数 n 和 k。

输出描述:

  • 对于每组测试用例,输出扩充n-1次后的序列中第 k 个元素的值。

 

eg:
input:
3 2
output:
2

eg2:
inpput:
4 8
output:
4

 

思路:递归 + 子问题拆解

 

 

 

import java.util.Scanner;

public class Main {
   static long f(long x, int y) { if (y == 1) return 1; // 第一层是1 long len = (1L << y) - 1; if (x == (len + 1) / 2) return y; if (x <= len / 2) return f(x, y - 1); return f(x - (len + 1) / 2, y - 1); } static void solve(int u) { Scanner scanner = new Scanner(System.in); long n = scanner.nextLong(); long k = scanner.nextLong(); System.out.println(f(k, (int) n)); } public static void main(String[] args) { int T = 1; for (int i = 1; i <= T; i++) { solve(i); } } }

  

 米哈游2023041501

题目描述

任何正整数都可以用若干个不相等的3的幂次方的和或差表示。给定一个正整数x,请找到一种由若干个不相等的3的幂次方的和或差表示x的方式,并按照每一项从大到小的顺序输出。

输入描述

一个正整数 x,1 ≤ x ≤ 109

 

输出描述

一个公式,最终的答案必须等于 x。公式中的每一项必须是 3 的幂,且不能有两项相同。

例如,18 必须输出为 27 - 9 而不能是 9 + 9。

示例

input:
30

output:
27+3

input:
300

output:
238+81-27+3

 

思路:

三进制+递归拆解

通过递归和借位的方式,可以将任意给定的正整数 &amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;lt;span class="katex"&amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;gt;&amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;lt;span class="katex-mathml"&amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;gt;&amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;lt;span class="katex-html"&amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;gt;&amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;lt;span class="base"&amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;gt;&amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;lt;span class="strut"&amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;gt;&amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;lt;span class="mord mathnormal"&amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;gt;x 表示为若干个 3 的幂次方的和或差。每次递归处理时,确定当前幂次范围,如果超出范围,则借用更高幂次继续处理,最终按要求输出结果。

 

 

 

 

 

package com.coedes.dfs.mhy202341501;

import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;

/**
 * @description:https://codefun2000.com/p/P1226
 * @author: wenLiu
 * @create: 2024/5/20 10:41
 */
public class Main {
    static int n;  // 输入的正整数
    static List<Integer> nums = new ArrayList<>();  // 用于存储表示 x 的若干个 3 的幂次方

    // 快速幂计算 a 的 b 次方  O(logb)
    static long pow(int a, int b) {
        long res = 1, t = a;
        while (b > 0) {
            if ((b & 1) == 1) res *= t;  // 如果 b 是奇数,将 t 乘到结果中
            t *= t;  // t 平方
            b >>= 1;  // b 右移一位
        }
        return res;
    }

    // 递归函数,将 x 表示为若干个 3 的整数幂的和或差
    static void get_val(int x) {
        int t = 1;  // 用于控制符号变换
        while (x > 1) {
            int p = (int) (Math.log(x) / Math.log(3));  // 计算 x 的 3 的对数,用于确定最高幂次
            if ((pow(3, p + 1) - 1) / 2 < x) {  // 判断 x 是否超出范围
                nums.add((int) pow(3, p + 1) * t);  // 借位,将 3^(p+1) 加入结果,并变换符号
                t *= -1;  // 符号变换
                x = (int) pow(3, p + 1) - x;  // 更新 x
            } else {
                nums.add((int) pow(3, p) * t);  // 直接将 3^p 加入结果
                x -= pow(3, p);  // 更新 x
            }
        }
        if (x != 0) nums.add(x * t);  // 如果 x 不为 0,将剩余部分加入结果
    }

    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        n = sc.nextInt();  // 读取输入的正整数
        get_val(n);  // 计算表示方法
        boolean flag = true;
        for (int x : nums) {
            if (flag) {
                System.out.print(x);  // 输出第一个元素
                flag = false;
            } else {
                if (x > 0) System.out.print("+");  // 如果元素为正,输出加号
                System.out.print(x);  // 输出元素
            }
        }
    }
}

  

 

 腾讯2023040202

要求根据特定的字符串编码规则反序列化得到二叉树,并计算所有节点的深度乘以权值之和。

具体规则如下:

  • 如果当前节点为空,则表示为字符 "X";
  • 如果当前节点不为空,则表示为 "(左子树编码)当前节点值(右子树编码)";
  • 左子树编码和右子树编码遵循相同的规则。

需要注意的是根节点的深度定义为1。

 

输入描述

输入字符串编码的二叉树

 

输出描述

深度*权值之和

 

input:
((X)2(X))1(((X)4(X))3((X)2(X)))
output:
29

 

思路: 双指针/DFS

 对于

((X)2(X))1(((X)4(X))3((X)2(X)))
可以转化为
1

 2                  3

            4                 2

 

可以观察到的规律是,树的深度 = 左括号数+1

可以从左往右遍历,遇到右括号则抵消掉一个左括号,直到当前元素为数值,然后计算器权值(sum+=sum*10 + s[j] (可能不止一位数))。

import java.util.*;

public class Main {
    static char[] s; // 输入的字符串编码的二叉树
    static long res = 0; // 深度*权值之和
    static int left = 0; // 记录左括号数量

    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        s = sc.nextLine().toCharArray(); // 读取输入的字符串编码的二叉树
        int m = s.length; // 输入字符串的长度
        for (int i = 0; i < m; i++) {
            if (s[i] == '(') left++; // 遇到左括号,左括号数量加1
            else if (s[i] == ')') left--; // 遇到右括号,左括号数量减1
            else if (s[i] == 'X') continue; // 如果是X,跳过,表示当前节点为空
            else {
                int j = i, sum = 0;
                while (j < m && s[j] >= '0' && s[j] <= '9') { // 循环找到当前节点的值
                    sum = sum * 10 + s[j] - '0'; // 将字符串转换成数字
                    j++;
                }
                res += sum * (left + 1); // 计算当前节点的深度*权值,并累加到结果中
                i = j - 1; // 更新i,跳过已处理的数字字符
            }
        }
        System.out.println(res); // 输出深度*权值之和
    }
}

  

 

1.5 树 

 题目描述

给定一个包含 &amp;amp;amp;amp;amp;amp;amp;amp;lt;span class="katex"&amp;amp;amp;amp;amp;amp;amp;amp;gt;&amp;amp;amp;amp;amp;amp;amp;amp;lt;span class="katex-mathml"&amp;amp;amp;amp;amp;amp;amp;amp;gt;&amp;amp;amp;amp;amp;amp;amp;amp;lt;span class="katex-html"&amp;amp;amp;amp;amp;amp;amp;amp;gt;&amp;amp;amp;amp;amp;amp;amp;amp;lt;span class="base"&amp;amp;amp;amp;amp;amp;amp;amp;gt;&amp;amp;amp;amp;amp;amp;amp;amp;lt;span class="strut"&amp;amp;amp;amp;amp;amp;amp;amp;gt;&amp;amp;amp;amp;amp;amp;amp;amp;lt;span class="mord mathnormal"&amp;amp;amp;amp;amp;amp;amp;amp;gt;n 个节点的树,树的每条边都连接两个节点。

要求找出所有可能的删除方式,使得删除这些节点后,剩下的每个部分都成为独立的部分(即,独立的小树)。

 

输入描述

  • 第一行输入一个正整数 &amp;amp;amp;amp;amp;amp;amp;amp;lt;span class="katex"&amp;amp;amp;amp;amp;amp;amp;amp;gt;&amp;amp;amp;amp;amp;amp;amp;amp;lt;span class="katex-mathml"&amp;amp;amp;amp;amp;amp;amp;amp;gt;&amp;amp;amp;amp;amp;amp;amp;amp;lt;span class="katex-html"&amp;amp;amp;amp;amp;amp;amp;amp;gt;&amp;amp;amp;amp;amp;amp;amp;amp;lt;span class="base"&amp;amp;amp;amp;amp;amp;amp;amp;gt;&amp;amp;amp;amp;amp;amp;amp;amp;lt;span class="strut"&amp;amp;amp;amp;amp;amp;amp;amp;gt;&amp;amp;amp;amp;amp;amp;amp;amp;lt;span class="mord mathnormal"&amp;amp;amp;amp;amp;amp;amp;amp;gt;n,代表树的节点数。
  • 接下来 &amp;amp;amp;amp;amp;amp;amp;amp;lt;span class="katex"&amp;amp;amp;amp;amp;amp;amp;amp;gt;&amp;amp;amp;amp;amp;amp;amp;amp;lt;span class="katex-mathml"&amp;amp;amp;amp;amp;amp;amp;amp;gt;&amp;amp;amp;amp;amp;minus;1&amp;amp;amp;amp;amp;amp;amp;amp;lt;span class="katex-html"&amp;amp;amp;amp;amp;amp;amp;amp;gt;&amp;amp;amp;amp;amp;amp;amp;amp;lt;span class="base"&amp;amp;amp;amp;amp;amp;amp;amp;gt;&amp;amp;amp;amp;amp;amp;amp;amp;lt;span class="strut"&amp;amp;amp;amp;amp;amp;amp;amp;gt;&amp;amp;amp;amp;amp;amp;amp;amp;lt;span class="mord mathnormal"&amp;amp;amp;amp;amp;amp;amp;amp;gt;n&amp;amp;amp;amp;amp;amp;amp;amp;lt;span class="mspace"&amp;amp;amp;amp;amp;amp;amp;amp;gt;&amp;amp;amp;amp;amp;amp;amp;amp;lt;span class="mbin"&amp;amp;amp;amp;amp;amp;amp;amp;gt;&amp;amp;amp;amp;amp;minus;&amp;amp;amp;amp;amp;amp;amp;amp;lt;span class="mspace"&amp;amp;amp;amp;amp;amp;amp;amp;gt;&amp;amp;amp;amp;amp;amp;amp;amp;lt;span class="base"&amp;amp;amp;amp;amp;amp;amp;amp;gt;&amp;amp;amp;amp;amp;amp;amp;amp;lt;span class="strut"&amp;amp;amp;amp;amp;amp;amp;amp;gt;&amp;amp;amp;amp;amp;amp;amp;amp;lt;span class="mord"&amp;amp;amp;amp;amp;amp;amp;amp;gt;1 行,每行输入两个正整数 &amp;amp;amp;amp;amp;amp;amp;amp;lt;span class="math math-inline"&amp;amp;amp;amp;amp;amp;amp;amp;gt;&amp;amp;amp;amp;amp;amp;amp;amp;lt;span class="katex"&amp;amp;amp;amp;amp;amp;amp;amp;gt;&amp;amp;amp;amp;amp;amp;amp;amp;lt;span class="katex-mathml"&amp;amp;amp;amp;amp;amp;amp;amp;gt;&amp;amp;amp;amp;amp;amp;amp;amp;lt;span class="katex-html"&amp;amp;amp;amp;amp;amp;amp;amp;gt;&amp;amp;amp;amp;amp;amp;amp;amp;lt;span class="base"&amp;amp;amp;amp;amp;amp;amp;amp;gt;&amp;amp;amp;amp;amp;amp;amp;amp;lt;span class="strut"&amp;amp;amp;amp;amp;amp;amp;amp;gt;&amp;amp;amp;amp;amp;amp;amp;amp;lt;span class="mord mathnormal"&amp;amp;amp;amp;amp;amp;amp;amp;gt;u 和 &amp;amp;amp;amp;amp;amp;amp;amp;lt;span class="math math-inline"&amp;amp;amp;amp;amp;amp;amp;amp;gt;&amp;amp;amp;amp;amp;amp;amp;amp;lt;span class="katex"&amp;amp;amp;amp;amp;amp;amp;amp;gt;&amp;amp;amp;amp;amp;amp;amp;amp;lt;span class="katex-mathml"&amp;amp;amp;amp;amp;amp;amp;amp;gt;&amp;amp;amp;amp;amp;amp;amp;amp;lt;span class="katex-html"&amp;amp;amp;amp;amp;amp;amp;amp;gt;&amp;amp;amp;amp;amp;amp;amp;amp;lt;span class="base"&amp;amp;amp;amp;amp;amp;amp;amp;gt;&amp;amp;amp;amp;amp;amp;amp;amp;lt;span class="strut"&amp;amp;amp;amp;amp;amp;amp;amp;gt;&amp;amp;amp;amp;amp;amp;amp;amp;lt;span class="mord mathnormal"&amp;amp;amp;amp;amp;amp;amp;amp;gt;v,代表节点 &amp;amp;amp;amp;amp;amp;amp;amp;lt;span class="math math-inline"&amp;amp;amp;amp;amp;amp;amp;amp;gt;&amp;amp;amp;amp;amp;amp;amp;amp;lt;span class="katex"&amp;amp;amp;amp;amp;amp;amp;amp;gt;&amp;amp;amp;amp;amp;amp;amp;amp;lt;span class="katex-mathml"&amp;amp;amp;amp;amp;amp;amp;amp;gt;&amp;amp;amp;amp;amp;amp;amp;amp;lt;span class="katex-html"&amp;amp;amp;amp;amp;amp;amp;amp;gt;&amp;amp;amp;amp;amp;amp;amp;amp;lt;span class="base"&amp;amp;amp;amp;amp;amp;amp;amp;gt;&amp;amp;amp;amp;amp;amp;amp;amp;lt;span class="strut"&amp;amp;amp;amp;amp;amp;amp;amp;gt;&amp;amp;amp;amp;amp;amp;amp;amp;lt;span class="mord mathnormal"&amp;amp;amp;amp;amp;amp;amp;amp;gt;u 和节点 &amp;amp;amp;amp;amp;amp;amp;amp;lt;span class="math math-inline"&amp;amp;amp;amp;amp;amp;amp;amp;gt;&amp;amp;amp;amp;amp;amp;amp;amp;lt;span class="katex"&amp;amp;amp;amp;amp;amp;amp;amp;gt;&amp;amp;amp;amp;amp;amp;amp;amp;lt;span class="katex-mathml"&amp;amp;amp;amp;amp;amp;amp;amp;gt;&amp;amp;amp;amp;amp;amp;amp;amp;lt;span class="katex-html"&amp;amp;amp;amp;amp;amp;amp;amp;gt;&amp;amp;amp;amp;amp;amp;amp;amp;lt;span class="base"&amp;amp;amp;amp;amp;amp;amp;amp;gt;&amp;amp;amp;amp;amp;amp;amp;amp;lt;span class="strut"&amp;amp;amp;amp;amp;amp;amp;amp;gt;&amp;amp;amp;amp;amp;amp;amp;amp;lt;span class="mord mathnormal"&amp;amp;amp;amp;amp;amp;amp;amp;gt;v 之间有一条边连接。

 

输出描述

  • 一个整数,代表符合题意的方案数。

 

输入:

5
1 2
1 3
2 4
4 5


输出:

3

  

 思路:出入度判断+建图(邻接表)

 题目要求删除一个节点,整个树被分为两部分 => 说明该节点边数恰好为2.

问题转化为对出度(或者入度,因为是无向图边是双向边)为2的节点进行记数。

可以先建图,后dfs遍历记数。

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;
import java.util.StringTokenizer;

/**
 * @description:from https://codefun2000.com/p/P1214
 * @author: wenLiu
 * @create: 2024/5/21 9:59
 */
public class Main {
    static int n, res;
    static List<Integer>[] g;

    public static void main(String[] args) throws IOException {
        BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
        n = Integer.parseInt(reader.readLine());
        g = new ArrayList[n + 1];
        for (int i = 1; i <= n; i++) {
            g[i] = new ArrayList<>();
        }

        // 边数 n-1
        for (int i = 1; i < n; i++) {
            StringTokenizer tokenizer = new StringTokenizer(reader.readLine());
            int a = Integer.parseInt(tokenizer.nextToken());
            int b = Integer.parseInt(tokenizer.nextToken());
            g[a].add(b);
            g[b].add(a);
        }

        res = 0;
        dfs(1, -1);
        System.out.println(res);
    }

    private static void dfs(int node, int parent) {
        if (g[node].size() == 2) {
            res++;
        }
        for (Integer neighbor : g[node]) {
            if (neighbor.equals(parent)) {
                continue;
            }
            dfs(neighbor, node);
        }
    }
}

  

 华为20220923

题目描述:

有一家云存储服务提供商,他们的存储系统采用主从模式以确保高可用性。当主节点发生故障时,系统会自动切换到备用节点。为了保证系统的稳定性,需要检测服务状态,并在必要时触发主备切换。

存储系统中的服务有依赖关系,每个服务最多依赖一个其他服务,且依赖关系不成环。某个服务故障时,其依赖的服务也会受到影响,可能导致更多服务故障。

需要确定检测顺序,以最大限度减少故障对业务的影响。首先检测对业务影响最大的服务,如果影响相同,则按节点编号升序检测。

 

输入描述

  • 第一行输入两个整数 n,代表业务节点总个数(1 ≤ n ≤ 100000)。
  • 接下来一行输入 n 个整数,第 i 个整数 fi 代表 i 依赖 fi(0 ≤ i < n)。
    • fi = -1,则表示节点 i 没有依赖。
    • 数据保证 fi ≠ i

输出描述

  • 一行输出 n 个整数,以空格隔开,代表最终的检测顺序。

 

input:
5
-1 -1 1 2 3

output:

4 3 2 1 0

  

思路:dfs+排序

  • fi 代表 i 依赖 fi 
    • 抽象为 边 fi->i
  • 排序规则
    • 子树节点数不同 :节点数多的影响大,排序靠前
    •  子树节点数相同 :按节点编号升序检测
import java.util.*;
import java.util.stream.IntStream;

public class Main {
    static int n; // 节点总数
    static int[] w, f, d, p; // w: 依赖数组, f: 子节点数量数组, d: 入度数组, p: 节点编号数组
    static List<Integer>[] g; // 邻接表表示的图

    // 深度优先搜索计算每个节点的子节点数量
    public static int dfs(int u) {
        f[u] = 1; // 每个节点自身算一个
        for (int x : g[u]) { // 遍历所有子节点
            f[u] += dfs(x); // 递归计算子节点的数量并累加
        }
        return f[u];
    }

    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        n = sc.nextInt(); // 输入节点总数
        w = new int[n]; // 依赖数组
        f = new int[n]; // 子节点数量数组
        d = new int[n]; // 入度数组
        p = new int[n]; // 节点编号数组
        g = new ArrayList[n]; // 初始化邻接表
        for (int i = 0; i < n; i++) {
            w[i] = sc.nextInt(); // 输入依赖节点
            g[i] = new ArrayList<>(); // 初始化每个节点的邻接表
        }

        // 构建图的邻接表和入度数组
        for (int i = 0; i < n; i++) {
            p[i] = i; // 初始化节点编号数组
            if (w[i] != -1) { // 如果节点有依赖
                d[i]++; // 增加入度
                g[w[i]].add(i); // 在依赖节点中添加当前节点
            }
        }

        // 对所有入度为0的节点进行DFS
        for (int i = 0; i < n; i++) {
            if (d[i] == 0) {
                dfs(i); // 计算每个节点的子节点数量
            }
        }

        // 将节点编号数组转换为Integer数组,以便进行排序
        Integer[] pWrapper = IntStream.of(p).boxed().toArray(Integer[]::new);

        // 根据子节点数量降序排序,如果子节点数量相同则按节点编号升序排序
        Arrays.sort(pWrapper, (a, b) -> {
            if (f[a] != f[b]) return f[a] > f[b] ? -1 : 1;
            return a - b;
        });

        // 输出排序后的节点编号
        for (int i = 0; i < n; i++) {
            System.out.print(pWrapper[i] + " ");
        }
    }
}

  

 

 

 

 

 

 

  

 

posted @ 2024-05-10 08:41  菠萝包与冰美式  阅读(8)  评论(0编辑  收藏  举报