本文主要是写一下笔者在掘金社区解决多米诺骨牌问题的思考与收获。
问题背景
今天,我花了一整个下午专注于一道非常有趣的算法题:关于多米诺骨牌的模拟。这道题目看似简单,但在解决过程中却让我经历了一次思维的深度锤炼,同时收获了巨大的成就感。
题目给出了这样一个情境:有一排多米诺骨牌,其中一些被推向左边(用字符 'L' 表示),一些被推向右边(用字符 'R' 表示),而还有一些则静止未动(用字符 '.' 表示)。当多米诺骨牌开始推倒时,每过一秒,推倒的骨牌会影响其相邻的骨牌,直到力的传播结束。如果一个骨牌同时受到两侧相等的力,它会因为力的平衡而保持竖立。我们的任务是模拟整个过程,并输出那些最终保持竖立的骨牌数量和位置。
这一题目的核心在于理解力的传播机制以及如何有效地模拟这个动态过程。虽然表面上它是一个简单的字符串处理问题,但实际上却隐藏着许多细节,特别是在处理边界条件和交替作用时。我深刻意识到,只有通过严谨的分析和细致的逻辑设计,才能真正解决这个问题。
2. 解题思路
2.1 理解力的传播
多米诺骨牌的核心在于“力”的传播。每个 'R' 或 'L' 都会在时间推移中向周围的骨牌传播影响。关键是处理两种特殊情况:
-
如果
'R'和'L'之间隔着一片'.',需要考虑它们之间的骨牌如何倒下:- 当间距为偶数时,这些骨牌会均匀地倒向两侧。
- 当间距为奇数时,正中间的骨牌会保持竖立,因为两边的力相等。
-
如果
'R'后面没有'L'或'L'前面没有'R',力会单向传播至字符串的边界。
2.2 使用双向扫描
在明确了力的传播逻辑后,我决定使用两次扫描来解决这个问题:
- 从左到右:处理所有
'R'推倒的力,模拟力从左向右传播的过程。 - 从右到左:处理所有
'L'推倒的力,模拟力从右向左传播的过程。
这种双向扫描的方法使得整个逻辑更加清晰,同时也能更高效地处理每个骨牌的状态。
3. 实现细节
在实现过程中,我决定将骨牌的状态存储在一个字符数组中,这样可以直接修改而不会引入新的字符串操作开销。整个过程可以分为以下几个步骤:
-
初始化状态:将字符串转为字符数组,便于操作,同时准备一个变量用于记录竖立的骨牌位置。
-
处理
'R'的影响:- 遍历字符串,当遇到
'R'时,记录其右侧的空白区域,直到遇到'L'或字符串的末尾。 - 如果遇到
'L',根据中间骨牌的数量来决定它们是倒向左右还是保持竖立。 - 如果没遇到
'L',则所有空白区域直接倒向右。
- 遍历字符串,当遇到
-
处理
'L'的影响:- 从右向左遍历字符串,当遇到
'L'时,记录其左侧的空白区域,直到遇到'R'或字符串的开头。 - 如果遇到
'R',这种情况在之前已经处理过,不需要再次修改。 - 如果没遇到
'R',则所有空白区域直接倒向左。
- 从右向左遍历字符串,当遇到
-
统计竖立骨牌:遍历最终的数组,记录所有
'.'的位置和数量。
4. 遇到的挑战与解决方案
4.1 边界条件的处理
- 在处理
'R'或'L'的作用范围时,必须格外小心边界。例如,当'R'位于字符串末尾,或者'L'位于字符串开头时,它们的作用范围直接延伸至边界。 - 解决方法是在扫描过程中随时检查边界条件,并在到达边界时终止传播。
4.2 奇偶性判断
- 当
'R'和'L'之间的距离为奇数时,中间的骨牌保持竖立;为偶数时,两边的骨牌均匀倒下。这一逻辑虽然简单,但在实现中需要确保索引计算正确。 - 我通过明确计算骨牌之间的间距并使用整除操作,有效地解决了这个问题。
4.3 输出格式
- 输出竖立骨牌的位置时,要求之间用空格分隔,但最后一个数字后不能有多余的空格。这一细节虽然不起眼,但对代码的严谨性提出了挑战。
- 我使用了条件判断避免了多余空格的输出。
5. 未来展望
这次的经历让我对模拟问题和动态状态更新有了更深刻的理解。未来,我希望能进一步挑战更复杂的模拟类题目,同时优化代码性能,尤其是大规模数据下的时间复杂度。相信通过不断努力,我会积累更多的算法技巧,在成长的道路上越走越远!
6. 实现代码
public static String solution(int num, String data) {
// 将字符串转为字符数组,方便修改
char[] dominoes = data.toCharArray();
StringBuilder result = new StringBuilder();
// 从左到右处理 'R'
int i = 0;
while (i < num) {
if (dominoes[i] == 'R') {
int j = i + 1;
while (j < num && dominoes[j] == '.') {
j++;
}
if (j < num && dominoes[j] == 'L') {
// R...L 的情况,平衡处理
int mid = (j - i - 1) / 2;
for (int k = 1; k <= mid; k++) {
dominoes[i + k] = 'R';
dominoes[j - k] = 'L';
}
i = j;
} else {
// 如果没有 'L',所有 '.' 都倒向右边
for (int k = i + 1; k < j; k++) {
dominoes[k] = 'R';
}
i = j;
}
} else {
i++;
}
}
// 从右到左处理 'L'
i = num - 1;
while (i >= 0) {
if (dominoes[i] == 'L') {
int j = i - 1;
while (j >= 0 && dominoes[j] == '.') {
j--;
}
if (j >= 0 && dominoes[j] == 'R') {
// 处理过的 R...L,不再修改
i = j;
} else {
// 如果没有 'R',所有 '.' 都倒向左边
for (int k = i - 1; k > j; k--) {
dominoes[k] = 'L';
}
i = j;
}
} else {
i--;
}
}
// 记录依然竖立的骨牌位置
StringBuilder standing = new StringBuilder();
int count = 0;
for (i = 0; i < num; i++) {
if (dominoes[i] == '.') {
count++;
if (standing.length() > 0) {
standing.append(" ");
}
standing.append(i + 1); // 骨牌位置从 1 开始
}
}
// 构建结果字符串
result.append(count).append("\n");
if (count > 0) {
result.append(standing);
}
return result.toString();
}
今天是充实的一天,也是让我深感满足的一天!💪😊