青训营刷题算法学习心得3 | 豆包MarsCode AI 刷题

48 阅读5分钟

学习方法与心得:多米诺骨牌问题

在解决豆包 MarsCode AI 刷题题库中的“多米诺骨牌平衡”问题时,我遇到了很大的困难。因为我自认为可以解决该问题的代码频频超时,而且改进了代码复杂度又,又发现自己的思路完全错误。一开始我将整个数列从左到右依次遍历,将骨牌分别向左向右推导,这导致了由题意本应同时倒下的骨牌变得从左到右依次倒下了。会导致本应左右不同方向而保持平衡的骨牌消失了。在咨询了豆包 MarsCode AI 后,我得到了让骨牌同时倒下的方法。

    /**
     * 小S玩起了多米诺骨牌,他排列了一行骨牌,并可能将某些骨牌向左或向右推倒。随着骨牌连锁反应的进行,
     * 一些骨牌可能因为左右两侧受力平衡而保持竖立。现在小S想要知道在所有动作完成后,哪些骨牌保持竖立。
     * 给定一个表示骨牌初始状态的字符串,其中:
     * "L" 表示该位置的骨牌将向左倒。
     * "R" 表示该位置的骨牌将向右倒。
     * "." 表示该位置的骨牌初始时保持竖立。
     * 模拟整个骨牌倒下的过程,求出最终仍然保持竖立的骨牌的数目和位置。
     */

package JuejinAiCodes;

import java.util.ArrayList;

public class DominoBalance {

    public static String solution(int num, String data) {
        char[] charArray = data.toCharArray();
        char[] tempArray = new char[num];

        while (true) {
            boolean hasChange = false;
            System.arraycopy(charArray, 0, tempArray, 0, num);

            for (int i = 0; i < num; i++) {
                if (charArray[i] == '.') {
                    boolean leftPush = (i > 0 && charArray[i - 1] == 'R');
                    boolean rightPush = (i < num - 1 && charArray[i + 1] == 'L');

                    if (leftPush && rightPush) {
                        // Balance: remains upright
                        tempArray[i] = '.';
                    } else if (leftPush) {
                        tempArray[i] = 'R';
                        hasChange = true;
                    } else if (rightPush) {
                        tempArray[i] = 'L';
                        hasChange = true;
                    }
                }
            }

            // Check if no changes occurred
            if (!hasChange) {
                break;
            }

            // Synchronize state
            System.arraycopy(tempArray, 0, charArray, 0, num);
        }

        // Find upright dominoes
        ArrayList<Integer> uprightPositions = new ArrayList<>();
        for (int i = 0; i < num; i++) {
            if (charArray[i] == '.') {
                uprightPositions.add(i + 1); // Convert to 1-based index
            }
        }

        if (uprightPositions.isEmpty()) {
            return "0";
        }

        return uprightPositions.size() + ":" + String.join(",", uprightPositions.stream().map(String::valueOf).toArray(String[]::new));
    }

    public static void main(String[] args) {
        //  You can add more test cases here
        System.out.println(solution(14, ".L.R...LR..L..").equals("4:3,6,13,14"));
        System.out.println(solution(5, "R....").equals("0"));
        System.out.println(solution(1, ".").equals("1:1"));
    }
}

问题背景

在这道题目中,我们模拟一排多米诺骨牌的倒下过程。骨牌有三种初始状态:

  • 'L':表示该骨牌向左倒;
  • 'R':表示该骨牌向右倒;
  • '.':表示该骨牌初始保持竖立。

规则如下:

  1. 骨牌的倒下会对相邻的骨牌施加推力。
  2. 如果一个竖立的骨牌同时受到左推和右推,保持平衡且不倒下。
  3. 我们需要模拟这一过程,直到骨牌状态稳定,并输出最终保持竖立的骨牌数量及其位置。

代码功能与结构

  • 输入

    • num:骨牌数量。
    • data:骨牌初始状态字符串。
  • 输出

    • 返回一个字符串,包含竖立骨牌的数量及其 1-based 的位置,格式为:数量:位置1,位置2,...。如果没有竖立的骨牌,返回 "0"

代码解析

1. 核心逻辑

主循环

  • 使用两个数组(charArraytempArray),通过模拟每一轮的骨牌倒下状态。

  • 遍历 charArray

    • 如果当前位置为 '.'(竖立),检查其左右是否有推力。

    • 根据左右推力状态,更新 tempArray 中的状态:

      • 左右同时推(平衡):保持竖立;
      • 仅受左推:变为 'R'
      • 仅受右推:变为 'L'
  • 若一轮中没有状态变化(hasChangefalse),模拟结束。

平衡判定

  • 一个骨牌同时受到左右推力时 (leftPush && rightPush),保持竖立状态。

最终统计竖立骨牌

  • 遍历稳定后的状态数组 charArray,记录所有为 '.' 的位置。

2. 代码细节与优化
  1. 双数组同步

    • 使用 System.arraycopy 在两数组之间同步状态,避免原数组被直接修改,同时提高效率。
  2. 减少遍历次数

    • 每轮检查是否发生状态变化(hasChange 标志位),如果没有变化直接跳出循环,提升性能。
  3. 位置转换

    • 最终输出位置转换为 1-based 索引,更贴近实际需求。

运行示例

  1. 输入num = 14, data = ".L.R...LR..L.."
    过程

    • 初始:.L.R...LR..L..
    • 第 1 轮:LL.RR..LR..L..
    • 第 2 轮:LL.RR.LLR.LL..
    • 稳定:LL.RR.LLR.LL..
      输出4:3,6,13,14
  2. 输入num = 5, data = "R...."
    过程

    • 初始:R....
    • 第 1 轮:RR...
    • 第 2 轮:RRR..
    • 第 3 轮:RRRR.
    • 稳定:RRRRR
      输出0
  3. 输入num = 1, data = "."
    输出1:1


时间与空间复杂度

  • 时间复杂度

    • 最外层循环:每轮模拟最多扫描 num 个骨牌,直到状态稳定(最坏情况下 O(num2)O(num^2) 次遍历)。
    • 因此总复杂度为 O(num2)O(num^2)。
  • 空间复杂度

    • 需要两个数组 charArraytempArray,占用 O(num)O(num) 空间。

学习心得

  1. 模拟过程与状态转换

    • 学会了用数组同步模拟状态变化,理解了如何通过双数组避免数据冲突。
    • 平衡判定(即左右推力相等时保持竖立)是逻辑核心,明确了如何在复杂条件中提取简单规则。
  2. 循环优化

    • 在模拟过程中,添加状态变化检测(hasChange)提前退出无效循环,减少了不必要的运算。
  3. 输出设计

    • 输出结果转化为 1-based 索引且格式化输出,提升了用户体验,学习到了如何处理结果转换。

学习计划与建议

  • 高效模拟:多练习模拟问题,尤其是涉及物理模型的题目(如骨牌效应、水流扩散等)。
  • 双数组与状态同步:掌握利用辅助数组模拟状态变化的技巧,避免直接修改原数据引起的错误。
  • 优化循环条件:在刷题时注意引入标志位等优化方法,避免多余的重复运算。

结合 AI 工具的帮助,我得意逐步从问题描述到代码实现,能够快速理解并掌握题目核心逻辑,同时锻炼思维的清晰性和代码的规范性。