假設有一個隨機數列{1,2,3,4,5....}
求其中相加為100的所數有組合,比方{{1,99},{1,2,97},{4,96}......}
自己想了一天,找不到解決辦法。求師兄們指點
10 个解决方案
類似於背包問題,遞歸來實現吧.
每個數字都可以選擇2中狀態,在組合里和不在組合里.
#define ARRAY_SIZE (10)
int pArray[ARRAY_SIZE] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int pPrint[ARRAY_SIZE];
int nPrintPos;
int nTempSum;
int nSum;
void CheckSum(int nPos)
{
int i;
if(nSum == nTempSum)
{
printf("%d = ", nSum);
for(i=0; i<nPrintPos-1; i++)
{
printf("%d + ", pPrint[i]);
}
printf(" %d\n", pPrint[i]);
return;
}
if(nPos < ARRAY_SIZE)
{
if(nTempSum+pArray[nPos] <= nSum)
{
nTempSum += pArray[nPos];
pPrint[nPrintPos++] = pArray[nPos];
CheckSum(nPos+1);
nPrintPos--;
nTempSum -= pArray[nPos];
}
CheckSum(nPos+1);
}
}
int main()
{
nTempSum = 0;
memset(pPrint, 0, sizeof(int)*ARRAY_SIZE);
nPrintPos = 0;
scanf("%d", &nSum);
CheckSum(0);
return 0;
}
垃圾代碼,僅供參考.
這樣的問題用回溯法解決比較合適。
回溯法的效率瓶頸在於剪枝函數與限界函數的效率。
為了使回溯法的效率高,應該注意:
1:要有一個好的搜索次序。因此,將隨機數列{1,2,3,4,5....}排序是有用的,因為這樣剪枝函數就不盲目,更有效率。
2:在判斷一個組合最后一個元素是否存在時,可用哈希表提高效率。比如:對一個三元組合,若已經確定前兩個為1,2,現在要確定第三個元97是否在數列中,可用哈希查找。
另外,樓主所謂的“隨機數列”,讓人很不解。如果“隨機數列”中有兩個是一樣的數,並且這個數包含在某個組合A中,那么A是算一個,還是兩個?
帖子暫時不結貼,再加50分,希望更多的人來一起討論。
回溯法:
#include <iostream>
using namespace std;
int list[20]={1,8,71,98,2,45,3,9,11,85,73,
5,61,54,38,79,28,18,21,31};
int Stack[10];
int Stacklen=0;
int s=0;
int GetMaxLen( )
{
int i=0, s=0;
while(s+list[i]<100)
s+=list[i++];
return i;
}
void Sort( )
{
int i, j, temp;
for( i=0; i<20; ++i )
{
for( j=i+1; j<20; ++j )
{
if( list[i]>list[j] )
{
temp=list[i];
list[i]=list[j];
list[j]=temp;
}
}
}
}
void Found( int f, int len, int sum )
{
int i;
if( len==0 )
{
if( s==100 )
{
cout<<"{";
for( i=0; i<Stacklen-1; ++i )
cout<<Stack[i]<<",";
cout<<Stack[i]<<"}"<<endl;
}
return ;
}
i=f;
while( list[i]+s<=100 && i<20 )
{
Stack[Stacklen]=list[i];
s+=list[i];
++Stacklen;
if( Stacklen==1 )
Found(f+1, len-1, sum-list[i]);
else
if(Stack[Stacklen-2]<Stack[Stacklen-1])
Found(f+1, len-1, sum-list[i]);
--Stacklen;
s-=list[i];
++i;
}
}
int main()
{
Sort();
int i, Maxlen=GetMaxLen();
for( i=2; i<=Maxlen; ++i )
Found( 0, i, 100 );
system("PAUSE");
return 1;
}
如果要找比回溯法更好的算法,那就可能要消耗空間來換取時間了。
記f(start,len,sum)為以在排完序的數列中序號為start首元,長度為len,和為sum的所有組合的集合。
注意到:回溯法重復解了許多子問題,為了避免這個問題,可以將子問題的解保存在一個表中,這個思路就是動態規划了。
比如:一個組合長度為6,已經確定頭3個數,頭3個中最大者在排完序的數列中序號是j,頭3個數的和為20.如果在以前的搜索中,f(j+k,3,80)(0<k<100-j)已經知道,那么將頭三個數和f(j+k,3,80)連接起來輸出就可以了。
但是,由於樓主要找的是所有組合,保存的解是相當多的,空間消耗太大。
有一個減少空間的辦法:用一位表示一個元在一個組合中的狀態:0表示不在這個組合中;1表示在這個組合中。設數列長度為L,那么任何一個組合都只需要L/8個字節。
==========================================
那復雜度是2^n了,有沒有更好的算法那?
==========================================
其實我上面說的回溯法,就已經能在8秒鍾之內給出全部解了。雖然復雜度是O(2^n),但是,通過剪枝,其實數據量並不大,我用從1到100的長度為100的自然數數列進行測試,用文件輸出,結果只用了7秒鍾,就得出全部的組合,總個數為444793個。
附上我的測試用的程序:
#include <iostream>
#include <fstream>
#include <windows.h>
using namespace std;
ofstream file("data.txt");
const int Max=100;
const int Sum=100;
int list[Max];
int Stack[Max];
int Stacklen=0;
int s=0;
int c=0;
int GetMinLen( )
{
int i=Max-1, s=0, len=0;
while( s+list[i]<Sum && i>=0 )
s+=list[i--],++len;
return len;
}
int GetMaxLen( )
{
int i=0, s=0;
while(s+list[i]<Sum && i<Max)
s+=list[i++];
return i;
}
void Found( int f, int len, int sum )
{
int i;
if( len==0 )
{
if( s==Sum )
{
file<<"{";
for( i=0; i<Stacklen-1; ++i )
file<<Stack[i]<<",";
file<<Stack[i]<<"}"<<endl;
++c;
}
return ;
}
i=f;
while( list[i]+s<=Sum && i<Max )
{
Stack[Stacklen]=list[i];
s+=list[i];
++Stacklen;
if( Stacklen==1 )
Found(f+1, len-1, sum-list[i]);
else
if(Stack[Stacklen-2]<Stack[Stacklen-1])
Found(f+1, len-1, sum-list[i]);
--Stacklen;
s-=list[i];
++i;
}
}
int main()
{
int i;
for( i=0; i<Max; ++i )
list[i]=i+1;
int time=GetTickCount();
int Maxlen=GetMaxLen();
int Minlen=GetMinLen();
for( i=Minlen; i<=Maxlen; ++i )
Found( 0, i, Sum );
cout<<"Total :"<<c<<endl;
cout<<"Use time :"<<GetTickCount()-time<<" ms\n";
cout<<"請打開文件'data.txt'查看結果!\n";
system("PAUSE");
return 1;
}