[week3]选数问题——DFS搜索算法

目录

  • 题意
    • Input
    • Output
    • 数据样例
    • 提示
  • 分析
  • 总结
  • 代码


题意

Given n positive numbers, ZJM can select exactly K of them that sums to S. Now ZJM wonders how many ways to get it!

【给定n个正数,ZJM可以选择正好K个和s的数,现在ZJM想知道有多少种方法可以得到它!】


Input

The first line, an integer T<=100, indicates the number of test cases. For each case, there are two lines. The first line, three integers indicate n, K and S. The second line, n integers indicate the positive numbers.

【第一行是一个整数T<=100,表示测试用例的数量。对于每种情况,有两行。第一行,三个整数代表n K s。第二行,n个整数代表正数。】

Output

For each case, an integer indicate the answer in a independent line.

【对于每种情况,一个整数在独立的行中表示答案。】


数据样例

[week3]选数问题——DFS搜索算法_第1张图片


提示

Remember that k<=n<=16 and all numbers can be stored in 32-bit integer.

【记住,k<=n<=16,所有数字都可以存储为32位整数】


分析

选数问题可利用DFS搜索算法解决,并且需要可行性剪枝来优化算法。


  • 什么是DFS?

DFS(深度优先搜索)与BFS一样,是一种图的搜索算法。这种算法是针对一条路径上的相连节点依次遍历持续进行搜索,当受到阻塞时依次返回之前的节点,寻找新路径,直到找到目标节点或遍历完所有节点。

通俗点说可以看作一个小人‍♂️选了众多分岔路中的一条一直走下去,当他走到死胡同的时候,再依次倒回之前的分岔点,尝试其他的路,直到他走到目的地或走完所有的路。


  • 为什么用DFS?

选数问题是从固定的一组数中选取指定个数的数字使其和等于题目要求。显然,满足要求的解不止一个,而题目需要我们究竟求出有多少解。

因此,这个问题可以看作一共有m条分岔路,其中n条可行。我们可以通过走每条路来计算可以走通的分岔路有多少条(即求出n)。


  • 数据结构的选择

由于本题需要临时存储当前选择的所有数(即当前走过路径中的所有节点),在选择数的过程中将反复进行增删操作。因此应该选择增删操作性能较优的容器——list(STL链表)——来临时储存当前选择的数,即本代码中的counting链表。

所有待选数只需用数组储存即可,本代码中使用了STL的vector容器,即num数组。


  • DFS算法实现

DFS算法多用递归实现,通过不断调用自身来实现走路径的操作,当遇到边界限制时返回即代表遇到阻塞。

DFS设计

  1. 递归函数参数
    函数传递两个参数,分别为计数器以及当前所选数的和与目标和的差值。

  2. 递归调用
    针对每个数,只会有选择与不选择两种操作。因此函数中存在两次参数不同的递归调用:差值不变、差值减去计数器所选数,分别代表不选择和选择当前计数器选中的数。

  3. 在两次递归调用之后,应分别跟上增删操作。

    1. 当递归调用为不选择该数
      该次递归返回到此处后向后继续运行时,代表在该节点不选择该数的后续情况已经运行结束(选择该分岔的路已经走完)。所以再回到该分岔路口,就应该进入到另一条分岔路中搜索(即选择该数)。
      因此,该调用之后应将当前计算器选中的数加入到临时链表中(在该次调用函数前并没有任何增删操作),并接着调用另一个递归函数。
    2. 当递归调用为选择该数
      同理,当返回并继续运行时,应将之前加入链表的数删除。

【小tip:此处理解应参考递归函数的运行本质】

  1. 边界限制

    1. 当选中数的个数以及总和满足要求时
      此时说明已经找到一条可行的路径,路径总数+1,并且可以返回之前的分岔点,搜索其他路径。
    2. 当计数器大于待选数总数时(限制数组下标边界)
      此时说明一条路径已经走到尽头,需要返回。

  • DFS优化——可行性剪枝

如果没有良好的边界限制,DFS算法往往会消耗过多时间在一些路径搜索中,并且还会得到无解的结果。但其实在搜索路径时,当一条路径明显不符合搜索目标时,可以及时返回,而不是必须要将每条路径都走完。

因此,剪枝这种优化操作在DFS算法设计中尤为重要。

剪枝包括:

  • 可行性剪枝
    将明显不可行的路径去掉。
  • 最优性剪枝
    将不是最优选择的路径去掉。
  • 记忆性剪枝
    记录已搜索的节点,避免重复搜索浪费时间。

【小tip:关于为什么需要记忆性剪枝?
在图的搜索算法中,每条路径都是由相连的节点组成,每个点接下来导向的路径会有多条。
DFS算法会选择该点的一条路径走到底,并回溯到该点又走其相连的另一条路径。重复该操作直到得到所有与与A点相连的路径的搜索结果。
因此在第二次搜索到该点时,不论接下来要选择走其相连的哪一条路,但实际上所有路径都已经搜索过了,因此不需要再第二次沿着该点继续搜索。
所以记录已经搜索过的点,就可以避免重复搜索,来优化算法性能。】

在选数问题的DFS递归搜索中,可能会出现当前选择的数的个数已经超过题目要求或是当前数的总和已经大于题目要求这些不合法情况,当不合法时,继续遍历所得的结果一定不为目标解。因此可以通过设置这两个可行性边界来限制,以此来优化算法,避免不必要的搜索。


总结

  1. STL中的各种容器都有最适合其使用的情况,应该熟悉这些容器的优化操作,来做具体合适的选择。
  2. 递归函数没有彻底搞得明明白白是不行的
  3. 终于记住了循环要清空容器,可以表扬一下

代码

//
//  main.cpp
//  lab-a
//
//
#include 
#include 
#include 
using namespace std;

vector<int> num;        //储存所有数字
list<int> counting;        //储存选取数字(链表结构:考虑增删操作性能)
int k=0,s=0,n=0,c=0;            //所需数字个数、目标和、数字总数、取数方式总数

void select(int i,int sum)          //dfs递归
{
//    cout<
    
    if( counting.size() == k && sum == 0 )      //当满足题意时,取数方式+1,并返回继续寻找其他情况
    {
        c++;
//        cout<
        return;
    }
    
    if( i >= n )                    //限制数组下标边界
        return;
    
    if( counting.size() > k )       //当选数个数大于k或当前选取数总和大于s时,返回
        return;
    if( sum < 0 )
        return;
    
    
    select(i+1, sum);               //不选择当前数
    counting.push_back(num[i]);     //将未选择的数加入链表中
    
    select(i+1, sum-num[i]);        //选择当前数
    counting.pop_back();            //将选择数删除
    
}


int main()
{
    int m=0,x=0;
    cin>>m;
    
    while( m-- )
    {
        cin>>n>>k>>s;
        for( int i = 0 ; i < n ; i++ )
        {
            cin>>x;
            num.push_back(x);
        }
        
        select(0, s);       //查找结果
        
        cout<<c<<endl;      //输出结果
        
        num.clear();            //将结构和计数都清零
        counting.clear();
        c=0;
        
    }
    
    
    return 0;
}


你可能感兴趣的:(实验,c++,算法,剪枝)