C语言实现组合总和问题的深度解析

一、问题描述

给定一个无重复元素的数组 arr 和一个目标数 target,找出 arr 中所有可以使数字和为 target 的组合。arr 中的数字可以无限制重复被选取。

例如:

  • 输入:arr = [2, 3, 6, 7]target = 7

  • 输出:

    2 2 3 
    7 

二、代码实现

以下是实现组合总和问题的C语言代码:

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#include <math.h>
#include <ctype.h>
#include <limits.h>

int n, target, k = 0;
int arr[10000] = { 0 };
int path[10000];

// qsort比较函数从小到大排序,用于快速剪枝
//比如,如果当前的总和加上当前元素已经超过target
//那么后面的更大的元素就不需要再考虑了。这可以剪枝。
void cmp(const void* a, const void* b) {
    return *(int*)a - *(int*)b;
}

// 回溯函数
void back(int start, int sum) {
    // 如果当前路径的和等于目标值,输出路径
    if (sum == target) {
        for (int i = 0; i < k; i++) {
            printf("%d ", path[i]);
        }
        printf("\n");
        return;
    }

    // 如果当前路径的和已经超过目标值,直接返回
    if (sum > target) {
        return;
    }

    // 从 start 开始遍历数组,避免重复
    for (int i = start; i < n; i++) {
        if (sum + arr[i] > target) {  // 剪枝:如果当前值已经超过目标值,跳过
            continue;
        }
        path[k] = arr[i];  // 将当前值加入路径
        k++;
        back(i, sum + arr[i]);  // 递归调用,允许重复使用相同值
        k--;  // 回溯,撤销当前选择
    }
}

int main() {
    // 输入数组大小和目标值
    scanf("%d %d", &n, &target);
    for (int i = 0; i < n; i++) {
        scanf("%d", &arr[i]);  // 输入数组元素
    }

    qsort(arr, n, sizeof(int), cmp);  // 对数组进行排序

    back(0, 0);  // 从索引 0 开始,当前路径和为 0

    return 0;
}

三、代码解析

1. 核心思想

组合总和问题的核心在于递归回溯。递归用于深度优先搜索,回溯用于撤销当前选择,尝试其他可能性。具体逻辑如下:

  • 使用一个数组 path 来存储当前的组合路径。

  • 使用 sum 来记录当前路径的和。

  • sum 等于目标值 target 时,输出当前路径。

  • sum 超过目标值时,直接返回,避免无效的递归。

  • 在每一层递归中,尝试从当前索引开始的所有可能值,并允许重复使用相同值。

2. 关键点解析

  • 排序优化:通过 qsort 对数组进行排序,可以提前剪枝,避免无效的递归调用。

  • 剪枝操作:如果当前值加上 sum 已经超过目标值,则跳过后续所有值。

  • 回溯操作:在每次递归返回时,撤销当前选择,恢复路径状态。

递归树示意图

数组 [2, 3, 6, 7] 和目标值 target = 7 的递归树结构示意图。

                     [0, 0] (sum=0)
                      /  |   |   \
                     /   |   |    \
                [2, 0]  [3, 0] [6, 0] [7, 0]
                (sum=2) (sum=3) (sum=6) (sum=7)
                /  |       |          |
               /   |       |          |
          [2, 2]  [2, 3]   [3, 3]     [7] ✔️
         (sum=4) (sum=5)  (sum=6)    (sum=7)
          /  |       |       |
         /   |       |       |
     [2, 2, 2]   [2, 2, 3]  [3, 3, 3]
    (sum=6)    (sum=7) ✔️  (sum=9) ✕
      /  |       |
     /   |       |
 [2, 2, 2, 2]  [2, 2, 3]
(sum=8) ✕   (sum=7) ✔️

然后再来看看

 组合总和问题与全排列问题的异同

相同点
  1. 递归和回溯

    • 组合总和问题和全排列问题都使用了递归和回溯的思想。通过递归搜索所有可能的解,并在找到一个解后回溯,尝试其他可能性。

    • 两者都依赖于深度优先搜索(DFS)来探索所有可能的路径。

  2. 数组排序

    • 在某些情况下,两者都可能需要对数组进行排序。排序可以帮助优化算法性能,例如通过剪枝减少不必要的递归调用。

  3. 路径记录

    • 两者都使用了一个路径数组(如 path)来记录当前的解路径,并在递归过程中不断更新这个路径。

不同点
  1. 问题目标

    • 全排列问题:目标是生成一个数组的所有可能排列。每个排列是数组元素的一种重新排列,且每个元素在每个排列中只能出现一次。

    • 组合总和问题:目标是找到所有可能的组合,使得这些组合的和等于目标值。数组中的元素可以重复使用,但组合本身不能重复。

  2. 递归逻辑

    • 全排列问题:递归时需要遍历所有未使用的元素,并将它们加入当前排列。递归完成后回溯,撤销当前选择。

    • 组合总和问题:递归时从当前索引开始,允许重复使用相同值。如果当前路径的和超过目标值,则直接返回。

  3. 剪枝操作

    • 全排列问题:通常不需要剪枝,因为每个排列都是合法的。

    • 组合总和问题:可以通过排序和剪枝优化。如果当前路径的和已经超过目标值,或者加上某个值后超过目标值,则可以直接跳过。

  4. 输出结果

    • 全排列问题:输出所有可能的排列,每个排列包含数组的所有元素。

    • 组合总和问题:输出所有可能的组合,每个组合的和等于目标值。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值