qq群里的一个同学发了一个这样的题目:
给定一个数t,以及n个整数,在这n个数中找到和为t的所有组合,例如t=4,n=6,这6个数为【4,3,2,2,1,1】,这样的不同组合他们的和为4: 4, 3+1 ,3+1,(有两个1,所以有两个3+1) 2+2 , and 2+1+1,2+1+ ,请设计一个高效算法实现。
看到这个问题,首先想到的就是递归,思路是这样:实现一个这样的函数:fun(DataList,Sum);即从DataList这一列数据中取出若干个数,使得它们的和为Sum。在函数内部,从DataList中取一个数出来,这个数的值为value,若value==Sum,那么找到了答案(这是一个出口)(如何输出答案呢?需要维护一个栈,每取出一个数,把这个数进栈,每一次回朔,出一次栈,在找到答案时,输出这个栈),若这个数小于t,那么把这个数从DataList中删掉,再调用fun(DataList,Sum-value),然后,记住把DataList复原(把取出来的这个数又加到DataLIst中原来的位置)。
如何把取出来的这个节点从DataList中删除,然后又复原?想象一下,感觉整个搜寻答案的过程就是不断的抽取节点和复原节点的过程
在
我这里仅仅只用到了前面几句话: “假设x指向双向链的一个节点;L[x]和R[x]分别表示x的前驱节点和后继节点。每个程序员都知道如下操作:
L[R[x]] ← L[x], R[L[x]] ← R[x]
|
(1)
|
是将x从链表删除的操作;但是只有少数程序员意识到如下操作:
L[R[x]] ← x, R[L[x]] ← x
|
(2)
|
是把x重新链接到双向链中。
”
所以,节点的出列和入列就可以按照这种链表方法来做,为什么不用一个标志数组呢来表示每一个节点是否在列中呢?因为用链表的话会减少大量无谓的判断,如果总共100个节点,只有10个节点在列中(已经抽取了90个节点了,即递归到底90层了),用标志数组的话仍然得判断100次,而用链表的话就只需10次了。
这种方法是穷举所有可能的组合,即:c(1,n)+ c(2,n)+ c(3,n)+ 。。。+ c(n,n)。(在这里c(a,b)为 b 中取 a 的组合) (2010-6-2注:这里错了,并没有那么低效,比如,要求sum为4的一群数,取出来两个数后就已经大于或等于4了,就不会再拿这两个数与其他的数进行组合了(即直接打印结果或者return,而不是继续往下递归))
c(1,n)的所有情况在第一层求出,所以第一层有n个判断,
c(2,n)的所有情况在第二层求出,所以第二层有c(2,n)个判断。
。。。 。。。
c(i,n)的所有种情况在递归的第i层求出,第i层判断的次数为c(i,n)
在第n层只有一次判断了(c(n,n) == 1)。
就这么做吧,我还想不到更高效的算法,有人知道的话,希望能指点一下。核心代码是这一段:
int nodeFilter(int EntryPoint,int sum)
{
for (int i=EntryPoint; i < NUM; i = R[i]) //参数EntryPoint在此处用到,递归形成的搜索树是所有的组合。
{
record[recordEnd++] = data[i];
//get an answer,print it.
if (sum == data[i])
{
PrintIntArray(record,recordEnd);
}
//try,go forward.
else if(sum > data[i])
{
//
//这一段是关键
//pickup the node (remove node i form the list)
L[R[i]] = L[i];
R[L[i]] = R[i];
//go forward.
nodeFilter(R[i],sum - data[i]); //递归。注意第一个参数是传R[i]进去,这样,递归形成的搜索树是所有的组合
//如果传进去的是R[-1],那么求出来的是所有符合要求的排列,这时,该函数
//的实现是不需要这个参数的。
//restore node i to the list.
L[R[i]] = i;
R[L[i]] = i;
//
}
//backtracking
--recordEnd;
}
return 1;
}
以下是实现的所有代码:
// DataSum.cpp : Defines the entry point for the console application.
#include <iostream>
#include <vector>
// #include <algorithm> //for for_each
#include <memory>
//
// 从文本输出
#include <fstream>
std::ofstream g_ofResult("./record.txt",std::ios::trunc);
//#define cout ::g_ofResult //开启此行输出到文本
//
using namespace std;
#define NUM 13
#define SUM 6
int data[NUM]; //数据,DataList
int HeadAndNext[NUM+1];
//left数组和right数组(L[NUM],R[NUM],即记录前驱节点和后继节点)
int L[NUM];
#define R (HeadAndNext+1) //R[-1]为起始节点,R[n]表示节点n的后继节点
int record[NUM]; //记录所求的的答案
int recordEnd; //栈顶
void InputData(int a[],size_t size); //输入数据
int nodeFilter(int EntryPoint,int sum); //递归函数,算法就在这里面
void PrintIntArray(int a[],size_t size); //输出一个整型数组
int main(int argc, char* argv[])
{
//Initialize
recordEnd = 0;
InputData(data,NUM);
cout<<"Given data:"<<endl;
PrintIntArray(data,NUM);
//initialize the doubly linked list.
R[-1] = 0; //起始节点为节点0
for (int i=0; i<NUM; ++i)
{
L[i] = i-1;
R[i] = i+1;
}
//start searching...
cout<<"start searching for the answers,to make the sum of their value equal to "<<SUM<<" ..."<<endl;
nodeFilter(R[-1],SUM);
system("pause");
return 0;
}
int nodeFilter(int EntryPoint,int sum)
{
// for (int i=R[-1]; i < NUM; i = R[i]) //开启此行求出所有排列,此时该函数不需要参数EntryPoint
for (int i=EntryPoint; i < NUM; i = R[i]) //开启此行求出所有组合,参数EntryPoint在此处用到。
{
record[recordEnd++] = data[i];
//get an answer,print it.
if (sum == data[i])
{
PrintIntArray(record,recordEnd);
}
//try,go forward.
else if(sum > data[i])
{
//
//这一段是关键
//pickup the node (remove node i form the list)
L[R[i]] = L[i];
R[L[i]] = R[i];
//go forward.
nodeFilter(R[i],sum - data[i]);
//restore node i to the list.
L[R[i]] = i;
R[L[i]] = i;
//
}
//backtracking
--recordEnd;
}
return 1;
}
void PrintIntArray(int a[],size_t size)
{
for (int i = 0; i < size; ++i)
{
cout<<a[i]<<" ";
}
cout<<endl;
}
void InputData(int a[],size_t size)
{
// for (int i=0; i<size; ++i)
// {
// cin>>a[i];
// }
// int data[]={1,1,2,2,3,4};
int data[]={10,9,8,7,6,5,4,3,3,3,2,2,1};
memcpy(a,data,size*sizeof(int));
}