子集和数问题——回溯法(C++)

问题描述
已知(w1, w2, …, wn)和M,均为正数。要求找出wi的和数等于M的所有子集。

例如:若n=4,(w1,w2,w3,w4)=(11,13,24,7),M=31,则满足要求的子集是(11,13,7)和(24,7).

分析

子集和数问题解的一种表示方法

  • 解由n-元组(x1, x2, …, xn)表示;
  • 显式约束条件xi∈{0,1},1≤i≤n,如果没有选择Wi,则xi=0;如果选择了Wi,则xi=1。于是上面的解可以表示为(1,1,0,1)和(0,0,1,1);
  • 隐式约束条件(xi× wi)的和数为M
  • 解空间的大小为2n个元组
#include<stdio.h>
// 宏定义最大个数MAX
#define MAX 10000
// 设置集合个数
int data[MAX] ;
// 是否选择该元素(True或False)对应与data集合的映射v
bool v[MAX] ;
// 集合元素的个数n,用户输入的子集和目标值c
int n , c ;


// 回溯函数traceback
bool traceback(int n)
{
// p可以看成是一个指针pointer,或者更准确来说应该是一个游标
// 作为我们[1]数组data和[2]data数组映射子集选中数组v的[游标]
// sum是当前正在跑着的子集的和的值

// 对p和sum进行初始化
// 注:p是游标,在C语言中,数组元素下标默认从0开始
int p = 0 ,sum = 0 ;

// 判断当前游标的位置
// 游标位置p<0则退出循环
while(p>=0)
{
/* 选择新进来的元素 */
// 当v[p]==0进入循环
if(!v[p])
{
// 子集中选中v[p]
v[p] = true ;
// 计算子集和,在sum上进行累加data[p]
sum += data[p] ;

// 判断当前累加的子集和sum和目标子集和c
// 如果相等
if(c == sum)
// 返回true
// 找到子集和的解
return true ;

// 如果当前累加的子集和sum > 目标值c
else if( c < sum)
{
// 不选择当前的a[p]值(重置v[p]为False)
// 并在当前子集和sum的基础上减去加上的a[p]
v[p] = false ;
sum -=data[p] ;
}
// 移动游标到下一位
p++ ;
}


/* 回溯过程(到最后一个元素都未找到解进入) */
// 检查游标位置p是否跑完数组data集合所有n个元素
// 如果当前游标值p大于等于集合数组data的元素个数n则进入回溯过程
if(p>=n)
{
// 到最后一个元素还没加到c,sum<c
// 最后一个元素为True
// 回溯到上次为False的地方在重新开始
while( v[p-1] )
{
// p游标自减
p-- ;
// 对当前v[p]赋值
v[p] = false ;
// 跑完整个data数组的n个元素
if(p<1) return false ;
}
//0 1 0 1 0 0 0 1

// 到最后一个元素sum>c
// 最后一个元素为False
// 回溯到上次为Ture的地方在重新开始
while( !v[p-1])
{
p-- ;
if(p<1) return false ;
}

// 回溯过程当前子集和sum也回溯到上次
sum -= data[p-1] ;
// 同时对当前回溯的v[p]重置为false(相当于不要这个元素)
v[p-1] = false ;
}
}

// 找不到满足子集和目标值的子集和解
// 退出回溯函数
return false ;
}

// 主函数
int main()
{
// 读入集合元素个数n,子集和目标值c
scanf("%d %d" , &n , &c) ;
// 依次读入集合中的n个元素
for(int i = 0 ; i < n ; i++)
scanf("%d" , &data[i]) ;

// 用回溯法计算子集和
// 判断是否存在结果(存在即traceback的值为True)
// 输出结果
if(traceback(n))
{
// traceback函数返回值为true
// 则得到目标值子集和的解
// 输出函数结果


int first = 1 ;
// i遍历v,范围是[0, n),即整个集合data数组以及数组v
for(int i = 0 ; i < n; i++)
// 判断v中第(i+1)个元素(v[i])的值是否为0(是否被选中为子集中的元素)
if(v[i])
{
// v[i]被选中了作为子集和中的元素

// first的值为非0
if(first)
// first变量重置为0
first = 1;
// first的值为0
else
printf(" ") ;
printf("%d " , data[i]) ;
}
printf("\n") ;
}
// traceback函数返回值为False
// 无法得到该目标值子集和的解
else
printf("No Solution!\n") ;

return 0 ;
}

引申

一、子集树

**子集树:**当所给的问题是从n个元素的集合S中找出满足某种性质的子集时,相应的解空间称为子集树。

例如,那个物品的0-1背包问题所相应的解空间树就是一颗子集树。这类子集问题通常有2^n
个叶节点,其节点总个数为2(n+1)-1。遍历子集树的任何算法均需要O(2n)的计算时间。
在这里插入图片描述

void backtrack (int t)  
{  
if (t>n) output(x);  
else  
for (int i=0;i<=1;i++) {  
x[t]=i;  
if (legal(t)) backtrack(t+1);  
}  
}  

二、排列树
排列树:当所给问题是确定n个元素满足某种性质的排列时,相应的解空间树称为排列树。

排列树通常有n!个叶子节点。因此遍历排列树需要O(n!)的计算时间。
在这里插入图片描述

void backtrack (int t)  
{  
if (t>n) output(x);  
else  
for (int i=t;i<=n;i++) {  
swap(x[t], x[i]);  
if (legal(t)) backtrack(t+1);  
swap(x[t], x[i]);  
}  
} 

猜你喜欢

转载自blog.csdn.net/weixin_44023658/article/details/105929754