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

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

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

分析数组

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

  • 解由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 ;
}

引伸

1、子集树函数

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

例如,那个物品的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);  
}  
}

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

排列树一般有n!个叶子节点。所以遍历排列树须要O(n!)的计算时间。
在这里插入图片描述xml

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]);  
}  
}