学习方法与心得:多米诺骨牌问题
在解决豆包 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':表示该骨牌向右倒;'.':表示该骨牌初始保持竖立。
规则如下:
- 骨牌的倒下会对相邻的骨牌施加推力。
- 如果一个竖立的骨牌同时受到左推和右推,保持平衡且不倒下。
- 我们需要模拟这一过程,直到骨牌状态稳定,并输出最终保持竖立的骨牌数量及其位置。
代码功能与结构
-
输入:
num:骨牌数量。data:骨牌初始状态字符串。
-
输出:
- 返回一个字符串,包含竖立骨牌的数量及其 1-based 的位置,格式为:
数量:位置1,位置2,...。如果没有竖立的骨牌,返回"0"。
- 返回一个字符串,包含竖立骨牌的数量及其 1-based 的位置,格式为:
代码解析
1. 核心逻辑
主循环:
-
使用两个数组(
charArray和tempArray),通过模拟每一轮的骨牌倒下状态。 -
遍历
charArray:-
如果当前位置为
'.'(竖立),检查其左右是否有推力。 -
根据左右推力状态,更新
tempArray中的状态:- 左右同时推(平衡):保持竖立;
- 仅受左推:变为
'R'; - 仅受右推:变为
'L'。
-
-
若一轮中没有状态变化(
hasChange为false),模拟结束。
平衡判定:
- 一个骨牌同时受到左右推力时 (
leftPush && rightPush),保持竖立状态。
最终统计竖立骨牌:
- 遍历稳定后的状态数组
charArray,记录所有为'.'的位置。
2. 代码细节与优化
-
双数组同步:
- 使用
System.arraycopy在两数组之间同步状态,避免原数组被直接修改,同时提高效率。
- 使用
-
减少遍历次数:
- 每轮检查是否发生状态变化(
hasChange标志位),如果没有变化直接跳出循环,提升性能。
- 每轮检查是否发生状态变化(
-
位置转换:
- 最终输出位置转换为 1-based 索引,更贴近实际需求。
运行示例
-
输入:
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
- 初始:
-
输入:
num = 5, data = "R...."
过程:- 初始:
R.... - 第 1 轮:
RR... - 第 2 轮:
RRR.. - 第 3 轮:
RRRR. - 稳定:
RRRRR
输出:0
- 初始:
-
输入:
num = 1, data = "."
输出:1:1
时间与空间复杂度
-
时间复杂度:
- 最外层循环:每轮模拟最多扫描
num个骨牌,直到状态稳定(最坏情况下 O(num2)O(num^2) 次遍历)。 - 因此总复杂度为 O(num2)O(num^2)。
- 最外层循环:每轮模拟最多扫描
-
空间复杂度:
- 需要两个数组
charArray和tempArray,占用 O(num)O(num) 空间。
- 需要两个数组
学习心得
-
模拟过程与状态转换:
- 学会了用数组同步模拟状态变化,理解了如何通过双数组避免数据冲突。
- 平衡判定(即左右推力相等时保持竖立)是逻辑核心,明确了如何在复杂条件中提取简单规则。
-
循环优化:
- 在模拟过程中,添加状态变化检测(
hasChange)提前退出无效循环,减少了不必要的运算。
- 在模拟过程中,添加状态变化检测(
-
输出设计:
- 输出结果转化为 1-based 索引且格式化输出,提升了用户体验,学习到了如何处理结果转换。
学习计划与建议
- 高效模拟:多练习模拟问题,尤其是涉及物理模型的题目(如骨牌效应、水流扩散等)。
- 双数组与状态同步:掌握利用辅助数组模拟状态变化的技巧,避免直接修改原数据引起的错误。
- 优化循环条件:在刷题时注意引入标志位等优化方法,避免多余的重复运算。
结合 AI 工具的帮助,我得意逐步从问题描述到代码实现,能够快速理解并掌握题目核心逻辑,同时锻炼思维的清晰性和代码的规范性。