困于循环的机器人与三数之和:算法揭示问题背后的逻辑与模式

博客引言:

在我们的日常生活中,无论是机器人的行为模式,还是数学中的三数之和问题,都隐藏着深刻的逻辑和模式。今天,我们将通过两个有趣的问题,探索如何用算法揭示这些逻辑与模式。

首先,我们将探讨困于循环的机器人问题,看看如何通过算法判断机器人是否会进入无限循环。接着,我们将分析三数之和问题,探讨如何高效地找到所有满足条件的三元组。通过这两个案例,你将看到算法如何在实际问题中揭示隐藏的逻辑与模式。

让我们一起进入算法的世界,探索这些逻辑与模式背后的奥秘!


博客正文:

一、困于循环的机器人:状态检测与循环判断

场景描述:
在一个无限的平面上,机器人最初位于(0, 0)处,面朝北方。机器人接受一系列指令,包括“G”(直行1个单位)、“L”(左转90度)、“R”(右转90度)。机器人会无限重复这些指令。我们需要判断机器人是否会进入无限循环,即永远无法离开某个区域。

算法核心:状态检测与循环判断
这个问题可以通过状态检测和循环判断来解决。机器人的状态由位置和方向组成。如果在某个周期内,机器人的状态重复出现,则说明它进入了循环。

详细分析:

  1. 初始化:记录机器人的初始位置(0, 0)和初始方向(北方)。
  2. 执行指令:按顺序执行指令,更新机器人的位置和方向。
  3. 状态检测:使用一个哈希表记录机器人在每个周期结束时的状态(位置和方向)。如果状态重复出现,则说明进入循环,返回true。
  4. 终止条件:如果机器人的位置在无限远的方向上移动,则返回false。

题目验证示例:

  • 示例 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;
    }

输出结果:

困于循环的机器人与三数之和:算法揭示问题背后的逻辑与模式_第1张图片 


二、三数之和:寻找不重复的三元组

场景描述:
给定一个整数数组nums,判断是否存在三个不同的元素nums[i], nums[j], nums[k],使得它们的和为0。返回所有满足条件的三元组,并确保结果中没有重复的三元组。

算法核心:排序与双指针法
这个问题可以通过排序和双指针法来解决。首先对数组进行排序,然后固定一个元素,使用双指针在剩下的元素中寻找满足条件的两个元素。为了避免重复解,需要跳过相同的元素。

详细分析:

  1. 排序:将数组排序,以便后续去重和双指针操作。
  2. 遍历数组:固定一个元素nums[i],作为三元组的第一个元素。
  3. 双指针法:使用两个指针left和right,分别指向i+1和数组末尾。计算三数之和,如果和为0,则记录三元组,并跳过相同的元素。如果和小于0,则左指针右移;如果和大于0,则右指针左移。
  4. 去重:在遍历过程中,跳过与前一个元素相同的元素,避免重复解。

题目验证示例:

  • 示例 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;
}

输出结果: 

困于循环的机器人与三数之和:算法揭示问题背后的逻辑与模式_第2张图片 


三、全方位对比:困于循环的机器人 vs 三数之和

对比维度 困于循环的机器人 三数之和
问题类型 状态检测与循环判断 数组处理与三元组查找
算法核心 哈希表、状态检测 排序、双指针法
复杂度 时间O(n^2),空间O(n) 时间O(n^2),空间O(1)
应用场景 机器人行为模式分析 数组处理、三元组查找
优化目标 判断机器人是否进入循环 寻找所有满足条件的三元组,并避免重复解

博客总结:

通过今天的分析,我们看到算法不仅仅是冰冷的代码,它还能帮助我们在实际问题中揭示隐藏的逻辑与模式。无论是困于循环的机器人,还是三数之和问题,背后的算法都在默默发挥作用,帮助我们更好地理解和解决实际问题。

希望这篇文章能让你对这些算法问题有更深入的了解,也期待你在生活中发现更多有趣的场景,用算法的视角去探索它们!


博客谢言:

感谢你的耐心阅读!如果你觉得这篇文章有趣,不妨在评论区分享你生活中遇到的算法问题,或者你认为可以用算法优化的地方。让我们一起用算法的视角去探索问题的奥秘!

你可能感兴趣的:(算法,数据结构,生活,c语言,机器人)