分割等和子集-动态规划+回溯-C语言实现

分割等和子集-回溯+动态规划

  1. 前言

分割等和子集,本质上是0-1选择问题,基于数组中的元素,面临两类不同的决定,选择当前元素和放弃当前元素,对于每个元素都实施上面策略,就可以形成一颗二叉树,在二叉树的某个或某几个结点上,所求元素的和恰好等于总和的一半,这种情况下,就可以判定集合可以分割为等和子集。

  1. 回溯方法

可以选择加法回溯,表示从0开始,选择相加或不相加;抑或选择减法,从最后一个元素开始,选择相减或者保持原状。我们可以对每个元素的选择保存在适当的数组中,当值达到一半的情况下,可以把此数组复制到最终答案数组当中去。

假设以减法做回溯,给定数组int nums[] = { 1, 5, 11, 5},求和得到sum=22,我们在过程中需要找寻是否有target=11的情况出现,如果出现则说明这个集合可以分割为等和子集,如果没有出现,则说明此集合无法分割为等和子集。

在这里插入图片描述

值得一提的是,上述两个选择实际上互为补集,最终答案只要选择其一即可。

  1. 回溯代码实现

回溯代码的实现依靠两个关键函数,partition函数的主要功能为求和,并创建选择数组,动态记录遍历过程中,对每个元素的选择情况,ans布尔数组主要是保存最终的结果。

回溯函数的包括两部分:

  • 如果当前的总值等于目标值,那么就把当前动态布尔数组里面的值拷贝到最终答案的布尔数组当中去,这是一个选择项;如果自始自终这个条件都无法满足,那么ans布尔数组中的初值false保持不变,表明集合无法等和分割。
  • 第二部分为回溯的关键部分,回溯的实质是深度优先遍历,在遍历执行之前或之后,对具体的值进行操作,但是并不改变递归的过程。回溯的关键是,找到合适的位置,对值进行“更新”与“恢复”。
/**
 * @file equal_partition.c
 * @author your name (you@domain.com)
 * @brief 
 * @version 0.1
 * @date 2023-03-12
 * 
 * @copyright Copyright (c) 2023
 * 
 */

#ifndef EQUAL_PARTITION_C
#define EQUAL_PARTITION_C
#include "equal_partition.h"

void partition(int *nums, int nums_size, bool *ans)
{
    int total;
    int sum;
    bool selection[nums_size];

    total = sum = nums_sum(nums, nums_size);

    memset(selection,false,sizeof(selection));

    partition_backtrack(nums_size - 1, total, nums, nums_size, sum, ans, selection);
}

void partition_backtrack(int i, int total, int *nums, int nums_size, int sum, bool *ans, bool *selection)
{
    if(total==sum/2)
    {
        memcpy(ans,selection,sizeof(bool)*nums_size);
    }

    if(i>=0 && total>0)
    {
        *(selection+i)=true; //select the current element (ith)
        partition_backtrack(i-1,total,nums,nums_size,sum,ans,selection);
        *(selection+i)=false; //deselect the current element(ith)
        partition_backtrack(i - 1, total-nums[i], nums, nums_size, sum, ans, selection);
    }
}


int nums_sum(int *nums, int nums_size)
{
    int i;
    int sum;

    for(sum=0,i=0;i<nums_size;i++)
    {
        sum+=nums[i];
    }

    return sum;
}
#endif

测试函数

/**
 * @file equal_partition_main.c
 * @author your name (you@domain.com)
 * @brief 
 * @version 0.1
 * @date 2023-03-12
 * 
 * @copyright Copyright (c) 2023
 * 
 */

#ifndef EQUAL_PARTITION_MAIN_C
#define EQUAL_PARTITION_MAIN_C
#include "equal_partition.c"

int main(void)
{
    int nums[] = { 1, 5, 11, 5}; // {2, 7, 9, 3};
    int num_size=sizeof(nums)/sizeof(int);
    bool ans[num_size];

    partition(nums,num_size,ans);


    printf("The maximum value is 2\n");

    getchar();
    return EXIT_SUCCESS;
}

#endif

对过程进行追踪,可以看到对每个元素选择的结果{false,false,true,false}, 表明{1,5,5}和{11}两个等和子集可以被分割。

在这里插入图片描述

  1. 动态规划实现

对此问题,动态规划方法比回溯高效,简洁。但是如果创造合适的dp数组,以及如何赋予dp数组适当的意义,就需要花大的力气进行分析,这个问题是否存在最长回文串的问题类似,它对状态的判断不是求最大值,也不是求最小值,而是搜索一类状态,并确认这个状态是否满足最终的要求。

基于以上分析,我们需要创建一个dp[i][j]数组,这个数组需要能清楚表明是否能从前i个物品中(包括i)进行不同的选择(选择或放弃)组合,使其和等于目标j,如果存在这样任意一个选择,那么dp[i][j]=true;如果所有的组合都无法满足要求,那么dp[i][j]=false.

基于上述分析和赋予dp[i][j]的伟大意义,状态转换方程就比较容易理解了。要求确定dp[i][j]的状态是为true,只要确定dp[i-1][j]或者dp[i-1][j-nums[i]]的状态即可,只要任意一个为true,那么dp[i][j]的总的判断就是true. 这个和0-1背包问题很类似,j-nums[i]表明剩余容量需要扣除当前的重量。

那么动态转换方程为:
d p [ i ] [ j ] = d p [ i − 1 ] [ j ] ∣ d p [ i − 1 ] [ j − n u m s [ i ] ] , w h e n   j > = n u m s [ i ] dp[i][j]=dp[i-1][j] | dp[i-1][j-nums[i]], when\ j>=nums[i] dp[i][j]=dp[i1][j]dp[i1][jnums[i]],when j>=nums[i]

d p [ i ] [ j ] = d p [ i − 1 ] [ j ]   w h e n   j < n u m s [ i ] dp[i][j]=dp[i-1][j]\ when\ j<nums[i] dp[i][j]=dp[i1][j] when j<nums[i]

上述两个方程就是状态转移方程。

有了状态转移方程,需要确立边界条件,基于之前的分析,如果j=0,对所有的i不做选择,那么在此条件下,数组的值dp[i][0]=true; 另外对于dp[0][nums[0]]对于前0个元素(包含第0个元素),我们可以选择nums[0]使其值等于j,在提条件下, dp[0][nums[0]]=true.

其它细节,在此不做赘述。

DP的代码实现,

/**
 * @file equal_partition.c
 * @author your name (you@domain.com)
 * @brief 
 * @version 0.1
 * @date 2023-03-12
 * 
 * @copyright Copyright (c) 2023
 * 
 */

#ifndef EQUAL_PARTITION_C
#define EQUAL_PARTITION_C
#include "equal_partition.h"

bool can_partition(int *nums, int nums_size)
{
    //Use bottom-up method to try to check if the array can be partitioned 
    //into two subsets with the sum same each
    int target;
    int sum;
    int max_value;
    int i;
    int j;

    sum=sum_nums(nums,nums_size);
    max_value=find_max(nums,nums_size);

    if(sum & 1) // if sum is odd, then it couldn't partition into two even half
    {
        return false;
    }

    target=sum/2;

    if(max_value>target)
    {
        return false;
    }

    bool dp[nums_size][target+1]; 

    memset(dp,0,sizeof(dp));
    /*
    dp[i][j] 表示从前ith选择物品,让其综合的值等于j,
    二维数组表示,包含n行,target+1列,其中dp[i][j]表示从数组[0,i]下标范围内
    选取若干个正整数元素(可以是0个),是否存在一种选取方案使得被选取的正整数
    的和等于j?
    初始的时候,dp的全部元素都为false

    这里的j相当于背包问题中的总重量

    - dp[i][0]=true; 表示如果不选择任何正整数,这时候选择的整数的总和等于0
    - 当 i==0 时,只有一个正整数nums[0]可以选择, dp[0][nums[0]]=true 可以被选取,
    */
   for(i=0;i<nums_size;i++)
   {
        dp[i][0]=true;
   }

   dp[0][nums[0]]=true;

   for(i=1;i<nums_size;i++)
   {
        for(j=1;j<=target;j++)
        {
            if(j>=nums[i])
            {
                dp[i][j]=(dp[i-1][j]) | (dp[i-1][j-nums[i]]);
            }
            else
            {
                dp[i][j]=dp[i-1][j];
            }
        }
   }

   return dp[nums_size-1][target];
}

bool can_partition_optimization(int *nums, int nums_size)
{
    int target;
    int sum;
    int max_value;
    int i;
    int j;
    
    sum=sum_nums(nums,nums_size);
    max_value=find_max(nums,nums_size);
    target=sum/2;

    if(sum & 1)
    {
        return false;
    }

    if(max_value>target)
    {
        return false;
    }

    bool dp[target+1];
    memset(dp,0,sizeof(dp));

    dp[0]=true;

    for(i=1;i<nums_size;i++)
    {
        for(j=target;j>=nums[i];j--) //in a reverse order
        {
            dp[j]=(dp[j]|dp[j-nums[i]]);
        }
    }

    return dp[target];
}

int sum_nums(int *nums, int nums_size)
{
    int sum=0;

    if(nums_size==0)
    {
        return 0;
    }
    else
    {
        sum+=(sum_nums(nums,nums_size-1)+nums[nums_size-1]);
    }

    return sum;
}

int find_max(int *nums, int nums_size)
{
    int max_value=INT_MIN;
    int i;

    for(i=0;i<nums_size;i++)
    {
        if(max_value<nums[i])
        {
            max_value=nums[i];
        }
    }

    return max_value;
}

#endif
  1. 总结

对于此问题,动态规划需要清晰定义dp[i][j]的含义,以及赋值的含义,否则即使清楚dp的可能形式,也无法正确对其base进行定义,另外dp数组经常需要比原值的范围大1,具体原因是,∅需要考虑。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值