在我们的日常生活中,无论是机器人的行为模式,还是数学中的三数之和问题,都隐藏着深刻的逻辑和模式。今天,我们将通过两个有趣的问题,探索如何用算法揭示这些逻辑与模式。
首先,我们将探讨困于循环的机器人问题,看看如何通过算法判断机器人是否会进入无限循环。接着,我们将分析三数之和问题,探讨如何高效地找到所有满足条件的三元组。通过这两个案例,你将看到算法如何在实际问题中揭示隐藏的逻辑与模式。
让我们一起进入算法的世界,探索这些逻辑与模式背后的奥秘!
场景描述:
在一个无限的平面上,机器人最初位于(0, 0)处,面朝北方。机器人接受一系列指令,包括“G”(直行1个单位)、“L”(左转90度)、“R”(右转90度)。机器人会无限重复这些指令。我们需要判断机器人是否会进入无限循环,即永远无法离开某个区域。
算法核心:状态检测与循环判断
这个问题可以通过状态检测和循环判断来解决。机器人的状态由位置和方向组成。如果在某个周期内,机器人的状态重复出现,则说明它进入了循环。
详细分析:
题目验证示例:
示例 1:
输入:instructions = "GGLLGG"
输出:true
解释:机器人最初在(0,0)处,面向北方。
“G”:移动一步。位置:(0,1)方向:北。
“G”:移动一步。位置:(0,2).方向:北。
“L”:逆时针旋转90度。位置:(0,2).方向:西。
“L”:逆时针旋转90度。位置:(0,2)方向:南。
“G”:移动一步。位置:(0,1)方向:南。
“G”:移动一步。位置:(0,0)方向:南。
重复指令,机器人进入循环:(0,0)——>(0,1)——>(0,2)——>(0,1)——>(0,0)。
在此基础上,我们返回true。
示例 2:
输入:instructions = "GG"
输出:false
解释:机器人最初在(0,0)处,面向北方。
“G”:移动一步。位置:(0,1)方向:北。
“G”:移动一步。位置:(0,2).方向:北。
重复这些指示,继续朝北前进,不会进入循环。
在此基础上,返回false。
示例 3:
输入:instructions = "GL"
输出:true
解释:机器人最初在(0,0)处,面向北方。
“G”:移动一步。位置:(0,1)方向:北。
“L”:逆时针旋转90度。位置:(0,1).方向:西。
“G”:移动一步。位置:(- 1,1)方向:西。
“L”:逆时针旋转90度。位置:(- 1,1)方向:南。
“G”:移动一步。位置:(- 1,0)方向:南。
“L”:逆时针旋转90度。位置:(- 1,0)方向:东方。
“G”:移动一步。位置:(0,0)方向:东方。
“L”:逆时针旋转90度。位置:(0,0)方向:北。
重复指令,机器人进入循环:(0,0)——>(0,1)——>(- 1,1)——>(- 1,0)——>(0,0)。
在此基础上,我们返回true。
#include // 提供输入输出函数支持
#include // 提供布尔类型支持
/**
* 判断机器人是否陷入循环路径
* @param instructions 指令字符串(G=直行/L=左转/R=右转)
* @return true-会循环,false-不会循环
*/
bool isRobotBounded(char* instructions) {
// 方向向量数组:北(0,y+1)、东(1,x+1)、南(0,y-1)、西(-1,x-1)
int dirs[4][2] = {{0, 1}, {1, 0}, {0, -1}, {-1, 0}};
int x = 0, y = 0; // 初始坐标原点
int dir = 0; // 初始方向北(对应dirs数组索引0)
/*==== 第一次指令执行 ====*/
for (char* c = instructions; *c != '\0'; ++c) { // 遍历每个指令字符
if (*c == 'G') { // 直行指令
x += dirs[dir][0]; // 根据当前方向更新x坐标
y += dirs[dir][1]; // 根据当前方向更新y坐标
} else if (*c == 'L') { // 左转指令
dir = (dir + 3) % 4; // 方向索引-1(等效+3取模)
} else if (*c == 'R') { // 右转指令
dir = (dir + 1) % 4; // 方向索引+1(顺时针)
}
}
/*==== 循环条件判断 ====*/
if (dir == 0) { // 执行指令后仍朝北
// 只有回到原点才会循环(否则会持续向北远离)
return (x == 0 && y == 0);
} else { // 方向发生改变时数学证明最多4个周期内可判断循环
/*==== 累计执行4次指令后的总位移 ====*/
int total_x = x, total_y = y; // 初始化第一次执行结果
int total_dir = dir; // 累计方向变化
// 额外执行3次指令序列(共4次)
for (int i = 0; i < 3; ++i) {
int curr_dir = total_dir; // 当前方向
int dx = 0, dy = 0; // 本次执行的偏移量
// 执行完整指令序列
for (char* c = instructions; *c != '\0'; ++c) {
if (*c == 'G') {
dx += dirs[curr_dir][0]; // 累加x偏移
dy += dirs[curr_dir][1]; // 累加y偏移
} else if (*c == 'L') {
curr_dir = (curr_dir + 3) % 4; // 左转方向调整
} else if (*c == 'R') {
curr_dir = (curr_dir + 1) % 4; // 右转方向调整
}
}
// 累加总偏移和最终方向
total_x += dx;
total_y += dy;
total_dir = curr_dir;
}
/* 数学证明:当方向改变后,最多4个周期:
1. 总方向变化为初始方向的倍数
2. 当4次执行后总位移为零则必循环 */
return (total_x == 0 && total_y == 0 && total_dir == 0);
}
}
/*==== 测试用例 ====*/
int main() {
// 示例1:GGLLGG → true
char* instructions1 = "GGLLGG";
printf("示例1输出: %s\n", isRobotBounded(instructions1) ? "true" : "false");
// 示例2:GG → false
char* instructions2 = "GG";
printf("示例2输出: %s\n", isRobotBounded(instructions2) ? "true" : "false");
// 示例3:GL → true
char* instructions3 = "GL";
printf("示例3输出: %s\n", isRobotBounded(instructions3) ? "true" : "false");
return 0;
}
输出结果:
场景描述:
给定一个整数数组nums,判断是否存在三个不同的元素nums[i], nums[j], nums[k],使得它们的和为0。返回所有满足条件的三元组,并确保结果中没有重复的三元组。
算法核心:排序与双指针法
这个问题可以通过排序和双指针法来解决。首先对数组进行排序,然后固定一个元素,使用双指针在剩下的元素中寻找满足条件的两个元素。为了避免重复解,需要跳过相同的元素。
详细分析:
题目验证示例:
示例 1:
输入:nums = [-1,0,1,2,-1,-4]
输出:[[-1,-1,2],[-1,0,1]]
解释:
nums[0] + nums[1] + nums[2] = (-1) + 0 + 1 = 0 。
nums[1] + nums[2] + nums[4] = 0 + 1 + (-1) = 0 。
nums[0] + nums[3] + nums[4] = (-1) + 2 + (-1) = 0 。
不同的三元组是 [-1,0,1] 和 [-1,-1,2] 。
注意,输出的顺序和三元组的顺序并不重要。
示例 2:
输入:nums = [0,1,1]
输出:[]
解释:唯一可能的三元组和不为 0 。
示例 3:
输入:nums = [0,0,0]
输出:[[0,0,0]]
解释:唯一可能的三元组和为 0 。
#include // 标准输入输出头文件
#include // 动态内存分配相关函数头文件
// 比较函数,用于qsort排序
int compare(const void* a, const void* b) {
return (*(int*)a - *(int*)b); // 升序排序
}
// 三数之和主函数
int** threeSum(int* nums, int numsSize, int* returnSize, int** returnColumnSizes) {
*returnSize = 0; // 初始化返回数组大小为0
if (numsSize < 3) return NULL; // 数组长度不足3直接返回空指针
qsort(nums, numsSize, sizeof(int), compare); // 快速排序原数组
/*=== 第一次遍历:计算满足条件的三元组数量 ===*/
int count = 0;
for (int i = 0; i < numsSize - 2; ++i) { // 遍历固定元素
if (i > 0 && nums[i] == nums[i-1]) continue; // 跳过重复的i元素
int left = i + 1; // 左指针初始位置
int right = numsSize - 1; // 右指针初始位置
while (left < right) { // 双指针遍历
int sum = nums[i] + nums[left] + nums[right];
if (sum == 0) { // 找到符合条件的三元组
count++;
// 跳过重复的左指针元素
while (left < right && nums[left] == nums[left+1]) left++;
// 跳过重复的右指针元素
while (left < right && nums[right] == nums[right-1]) right--;
left++; // 移动指针寻找下一个可能组合
right--;
} else if (sum < 0) { // 和太小,左指针右移
left++;
} else { // 和太大,右指针左移
right--;
}
}
}
if (count == 0) return NULL; // 无解则返回空指针
/*=== 内存分配阶段 ===*/
int** result = (int**)malloc(count * sizeof(int*)); // 分配结果数组
*returnColumnSizes = (int*)malloc(count * sizeof(int)); // 分配列宽数组
for (int i = 0; i < count; ++i) {
(*returnColumnSizes)[i] = 3; // 每个三元组固定3列
}
/*=== 第二次遍历:填充结果数组 ===*/
int index = 0; // 结果数组索引
for (int i = 0; i < numsSize - 2; ++i) {
if (i > 0 && nums[i] == nums[i-1]) continue; // 再次跳过重复i元素
int left = i + 1;
int right = numsSize - 1;
while (left < right) {
int sum = nums[i] + nums[left] + nums[right];
if (sum == 0) {
result[index] = (int*)malloc(3 * sizeof(int)); // 分配三元组内存
result[index][0] = nums[i]; // 填充第一个元素
result[index][1] = nums[left]; // 填充第二个元素
result[index][2] = nums[right]; // 填充第三个元素
index++;
// 跳过重复元素
while (left < right && nums[left] == nums[left+1]) left++;
while (left < right && nums[right] == nums[right-1]) right--;
left++;
right--;
} else if (sum < 0) {
left++;
} else {
right--;
}
}
}
*returnSize = count; // 设置返回数组大小
return result; // 返回结果数组指针
}
/*=== 测试用例 ===*/
int main() {
// 示例1:[-1,0,1,2,-1,-4] ----->[[-1,-1,2],[-1,0,1]]
int nums1[] = {-1, 0, 1, 2, -1, -4};
int size1 = sizeof(nums1)/sizeof(nums1[0]);
int returnSize1;
int* colSizes1 = NULL;
int** res1 = threeSum(nums1, size1, &returnSize1, &colSizes1);
printf("示例1输出:\n");
for (int i = 0; i < returnSize1; ++i) {
printf("[%d, %d, %d] ", res1[i][0], res1[i][1], res1[i][2]);
free(res1[i]); // 释放三元组内存
}
printf("\n");
free(res1); // 释放结果数组
free(colSizes1); // 释放列宽数组
// 示例2:[0,1,1]----->[]
int nums2[] = {0, 1, 1};
int size2 = sizeof(nums2)/sizeof(nums2[0]);
int returnSize2;
int* colSizes2 = NULL;
int** res2 = threeSum(nums2, size2, &returnSize2, &colSizes2);
printf("示例2输出:\n");
if (returnSize2 == 0) printf("[]\n");
free(res2); // 即使无结果也需释放指针
free(colSizes2);
// 示例3:[0,0,0]----->[[0,0,0]]
int nums3[] = {0, 0, 0};
int size3 = sizeof(nums3)/sizeof(nums3[0]);
int returnSize3;
int* colSizes3 = NULL;
int** res3 = threeSum(nums3, size3, &returnSize3, &colSizes3);
printf("示例3输出:\n");
for (int i = 0; i < returnSize3; ++i) {
printf("[%d, %d, %d] ", res3[i][0], res3[i][1], res3[i][2]);
free(res3[i]);
}
printf("\n");
free(res3);
free(colSizes3);
return 0;
}
输出结果:
对比维度 | 困于循环的机器人 | 三数之和 |
---|---|---|
问题类型 | 状态检测与循环判断 | 数组处理与三元组查找 |
算法核心 | 哈希表、状态检测 | 排序、双指针法 |
复杂度 | 时间O(n^2),空间O(n) | 时间O(n^2),空间O(1) |
应用场景 | 机器人行为模式分析 | 数组处理、三元组查找 |
优化目标 | 判断机器人是否进入循环 | 寻找所有满足条件的三元组,并避免重复解 |
通过今天的分析,我们看到算法不仅仅是冰冷的代码,它还能帮助我们在实际问题中揭示隐藏的逻辑与模式。无论是困于循环的机器人,还是三数之和问题,背后的算法都在默默发挥作用,帮助我们更好地理解和解决实际问题。
希望这篇文章能让你对这些算法问题有更深入的了解,也期待你在生活中发现更多有趣的场景,用算法的视角去探索它们!
感谢你的耐心阅读!如果你觉得这篇文章有趣,不妨在评论区分享你生活中遇到的算法问题,或者你认为可以用算法优化的地方。让我们一起用算法的视角去探索问题的奥秘!