1. 程式人生 > >一道筆試題:撈魚問題

一道筆試題:撈魚問題

題目:20個桶,每個桶中有10條魚,用網從每個桶中抓魚,每次可以抓住的條數隨機,每個桶只能抓一次,問一共抓到180條的排列有多少種 (也可求概率)。

分析一下

這道題其實可以這樣理解,假設我已經抓到了180條魚,而這180條魚來自20個桶中,反過來就是我們將這180條魚放到20個桶中且要保證每個桶的魚在(0-10)之間,有多少種方法。假設我們在前i個桶中抓取了k(0<=k<=10*i)條魚,那麼抓取180條魚的排列種數就等於在剩下的(20-i)個桶中抓取(180-k)條魚的方法加上前i個桶中抓取k條魚的方法。

再想一下

由於總共有200條魚,我們抓走了180條魚,那麼還剩下多少條呢?顯然是20條。再用上面的思維,我們就可以這樣想,將20條魚放回這20個桶中滿足每個桶中的魚(0-10),有多少种放法。這裡當然是一個排列問題而不是組合問題,因為我第一個桶放1條魚、第2個桶不放與第一個桶不放、第二個桶放1條魚,是兩種的不同的放法。

程式設計思想

思想其實跟完全揹包問題類似,我們首先考慮第20個桶放多少魚,那麼我們就可以減去最後一個桶放的魚數,再去考慮前面19個桶怎麼放剩下的魚。這樣形成了一個遞迴,我們就可以很快寫出如下程式碼。

//bucket,表示當前考慮到第幾個桶,當然首先考慮第20個桶
//fishN,表示當前有多少魚
//返回值表示有多少中方法
int Func(int bucketN,int fishN)
{
	if(bucketN<0 || fishN<0)return 0;//錯誤處理

	if(bucketN == 1){
		if(fishN>=0 && fishN<=10)//第1個桶裡面放0-10的魚,方法只有一種
			return 1;
		else{
			return 0;  //其他用0種方法
		}
	}

	int sum = 0;

	for(int i=0; i<=10; ++i){
		sum += Func(bucketN-1,fishN-i);//考慮前面bucket-1的組合,同時減掉當前放的魚
	}
	return sum;
}

遞迴優化

上面的程式碼,其實有很多重複計算。下面我們簡單的分析一下:

F(20,20) = F(19,19)+ F(19,18).......; //考慮第20個桶放一條魚和兩條魚的情況

F(19,19) = F(18,17)+.....;//第19個桶放2條魚  ①

F(19,18) = F(18,17)+.....;//第19個桶放1條魚  ②

     我們發現①和②都呼叫了F(18,17),但是它們確實各算各的,這樣就存在著很多類似的重複計算。該遞迴樹,基本全是重複的點,這樣時間複雜度被拖得很高。那麼,我們只要設計一個數組來把計算好的值儲存下來,這樣避免了很多重複的計算。程式碼如下:

//bucket,表示當前考慮到第幾個桶,當然首先考慮第20個桶
//fishN,表示當前有多少魚
//返回值表示有多少中方法
int dp[21][200] = {0};
int FuncOptimize(int bucketN,int fishN)
{
	if(bucketN<0 || fishN<0)return 0;

	if(bucketN == 1){
		if(fishN>=0 && fishN<=10)
			return 1;
		else{
			return 0;
		}
	}
	if(dp[bucketN][fishN] == 0){ //這個子過程沒有被計算我們採取呼叫遞迴計算
		for(int i=0; i<=10; ++i){
			dp[bucketN][fishN] += FuncOptimize(bucketN-1,fishN-i);
		}
	}
	
	return dp[bucketN][fishN];
}

下面我們用我們熟悉的斐波拉契序列做一個實驗,我們同樣的優化方式來做,看下程式跑得快了多少?

#include<iostream>
#include <windows.h>
using namespace std;
long long fibonacci[100];
long long Fibonacci(int n)
{
	if(n == 0)return 1;
	if(n == 1)return 1;

	return Fibonacci(n-1) + Fibonacci(n-2);
}

long long FibonacciOptimize(int n)
{
	if(n == 0)return 1;
	if(n == 1)return 1;

	if(fibonacci[n] == 0){
		fibonacci[n] = FibonacciOptimize(n-1) + FibonacciOptimize(n-2);
	}

	return fibonacci[n];
}


int main()
{
	DWORD seconds = GetTickCount();
	cout<<Fibonacci(40)<<endl;
	cout<<"優化之前:"<<(GetTickCount() - seconds)<<"ms"<<endl;
    seconds = GetTickCount();
	cout<<FibonacciOptimize(40)<<endl;
	cout<<"優化之後:"<<(GetTickCount() - seconds)<<"ms"<<endl;
	system("pause");
	return 0;
}


這個結果很是滿意啊。

動態規劃實現

們知道這種自頂向的遞迴,都可以轉換為動態規劃自底向上的思想。就是我們遞迴的值是有下面的值一步步壘起來的,那麼我只需要一開始就把下面的值算好,保持在那裡,然後再算上面的時候,直接拿過來用,這就很快了。據我瞭解,揹包問題的程式設計思路跟這道題是一摸一樣,只不過揹包走到第i種物品的時候,第i種物品的重量已經固定,而我們這道題是第i個桶取值是一個範圍,那我們只要把這裡範圍都掃一遍就好了

#include<iostream>
#include <windows.h>
using namespace std;

int dp[21][200] = {0};
int FuncDp(int bucketN,int fishN)
{
	int i,j,k;

	for(i=0; i<=10; ++i)
		dp[1][i] = 1;//初始值要弄好,不然後面就廢了

	for(i=2; i<=bucketN; ++i){
		for(j=0; j<=fishN; ++j){
			for(k=0; k<=10&&j-k>=0; ++k){//可以取0-10
				dp[i][j] += dp[i-1][j-k];
			}
		}
	}

	return dp[bucketN][fishN];
}

int main()
{
	cout<<FuncDp(20,180)<<endl;
    memset(dp,0,sizeof(dp));
	cout<<FuncDp(20,20)<<endl;
	system("pause");
	return 0;
}


    其實,程式碼還可以更簡練,仔細想想,就是初始化狀態的方法;其實初始化合法狀態完全可以這樣想,問題始終都是分解成子問題的,根據遞迴的實現方法,只有分解到0個桶裝0條魚才是合法的,那麼我們就初始化這一個狀態為合法即可,然後從第一個桶開始向上計算,程式碼如下:

#include <iostream>

using namespace std;

int dp[21][200];
int i, j, k;
 
void main()
{
    int bucketN, fishN;
    scanf("%d %d", &bucketN, &fishN);
 
    dp[0][0] = 1;  /* 初始化合法狀態 */
 
    for(int i = 1; i <= bucketN; ++i)  /* 從第一個桶開始 */
    {
        for(int j = 0; j <= fishN; ++j)
        {
            for(int k = 0; k <= 10 && j-k >= 0; ++k)
            {
                dp[i][j] += dp[i-1][j-k];
            }
        }
    }
    printf("%d\n",dp[bucketN][fishN]);
}

想不通我的空間優化怎麼不成功,類似揹包問題的優化,555555555555555555。有大神可以幫忙解釋一下啊?

#include<iostream>
#include <windows.h>
using namespace std;

int dp[200] = {0};
int FuncDp(int bucketN,int fishN)
{
	int i,j,k;

	for(i=0; i<=10; ++i)
		dp[i] = 1;

	for(i=2; i<=bucketN; ++i){
		for(j=fishN; j>=0; --j){//反著遍歷,防止覆蓋
			for(k=0; k<=10&&j-k>=0; ++k){//可以取0-10
				dp[j] += dp[j-k];
			}
		}
	}

	return dp[fishN];
}

int main()
{
	cout<<FuncDp(20,180)<<endl;
	memset(dp,0,sizeof(dp));
	cout<<FuncDp(20,20)<<endl;
	system("pause");
	return 0;
}