Cpp環境【Vijos1060】斯特林數:盒子與球

【問題描述】      

  n 個盒子排成一行(編號爲1..n)。你有A個紅球和B個藍球。球除了顏色沒有任何區別。你可以將球放進盒子。一個盒子可以同時放進兩種球,也可以只放一種,也可以空着。球不必全部放入盒子中。編程計算有多少种放置球的方法。

【輸入格式】      

  一行,n,A,B,用空格分開。

【輸出格式】      

  一行,輸出放置方案總數。

【輸入樣例】      

2 1 1

【輸出樣例】      

9

【樣例解釋】  

  用一對括號表示一個盒子,R表示紅色,B表示藍色,有如下9種方案:
   ( ), ( )
   (R ), ( )
   (B ), ( )
   (RB), ( )
   (R ), (B )
   (B ), (R )
   ( ), (R )
   ( ), (B )
   ( ), (RB)

【數據範圍】  

40% 數據滿足:n<=10,A+B<=10
100%的數據滿足:1<=n<=20 ,0<=A<=15 ,0<=B<=15
最後的答案不會超過 這裏寫圖片描述-1。

【來源】

NOIP提高組
Vijos1060原題傳送矩陣
重慶一中題庫原題傳送矩陣

【思路梳理】

  考試的時候因爲邊界問題糾結了很久=_+,想了一陣居然沒有想通所以交了對拍程序拿了60分走人,現在回想起來還是很虧,給自己在這裏留一個教訓。
  言歸正傳,關於這個題首先想到的肯定是回溯算法暴力枚舉,run(i,j,k)=目前在考慮第i個盒子,前i-1個盒子中已經放入了j個紅球,k個藍球,那麼依次枚舉在這個盒子中放入x個紅球,y個藍球(0≤x≤A-j,0≤y≤B-k)的可行方案數:

unsigned long long solve(int i,int j,int k)//考慮第i個盒子,目前已總共放入j個紅球,k個籃球
{
      if(i>n) return 1;//所有盒子都考慮完了,是一種可行方案
      long long cnt=0;//將各個可能的方案數加在一起求和
      for(int x=0;x<=A-i;x++)
      for(int y=0;y<=B-j;y++)
        cnt+=solve(i+1,i+x,j+y);
      return cnt;
}

  觀察數據範圍,因爲回溯的時間複雜度是O(AB^N),那麼也就是40分的樣子。方案數考慮高效算法,使用遞推的思想,先列出狀態函數以及狀態轉移方程:

  • f(i,j,k)=在前i個盒子中放入不超過j個紅球,不超過k個藍球的方案數。
  • 狀態轉移方程:這裏寫圖片描述
  • 邊界分析:f(0,i,j)=1,不放總會是一種方案,於是可以寫出如下的dp函數,時間複雜度爲O(n*):
void dp_1()
{
    //f(i,j,k)=f(i-1,j-m,k)+f(i-1,j,k-n) (0<=m<=j,0<=n<=k)

    for(int i=0;i<=A;i++)
    for(int j=0;j<=B;j++)   d[0][i][j]=1;

    for(int i=1;i<=n;i++)
    for(int j=A;j>=0;j--)
    for(int k=B;k>=0;k--)
    {
        unsigned long long t=0;
        for(int m=0;m<=j;m++)//第i個盒子裝m個紅球 
        for(int n=0;n<=k;n++)//第i個盒子裝n個藍球 
            t+=d[i-1][j-m][k-n];
        d[i][j][k]=t;
    }
}

觀察後可以發現,可以將t+=d[j-m][k-n]改爲t+=d[m][n](區別在於正着加和反着加)

如果你不是一個有志向的OIER,請自動忽略以下內容!


如上函數,五重循環雖然能夠完美解答此題,但是依然不能讓我們滿意:當n達到50,0≤A,B≤30左右時,dp_1是無法在1s內解答的;或者是有多種球,例如說有四種顏色的球、五種顏色的球……顯然我們還需要考慮一些更高效的算法:
這裏所謂的球除了顏色以外沒有任何區別,那麼問題可以分步完成,每一次都考慮一種顏色的球:對於此題的紅藍兩種球,先考慮放紅球再考慮放藍球,那麼最後的總方案數就等於這裏寫圖片描述,即在n個盒子中分步放入A個紅球和B個藍球,然後運用乘法原理乘起來。這樣一來就可以三維數組轉二維數組,循環減少爲三重,時間複雜度下降爲這裏寫圖片描述

int limit=max(A,B);
void dp_2()
{
    //狀態函數f(i,j)=在前i個盒子中放入不多於j個(紅/藍)球的方案數 
    //狀態轉移方程f(i,j)=f(i-1,j-m)(0<=m<=j)
    for(int i=0;i<=limit;i++)   d[0][i]=1;

    for(int i=1;i<=n;i++)
    for(int j=0;j<=limit;j++)
    {
        unsigned long long t=0;
        for(int m=0;m<=j;m++)//第i個盒子裝m個球 
            t+=d[i-1][j-m];
        //同樣可以使用:t+=d[i-1][m]

        d[i][j]=t;
    }
    cout<<(d[n][A]*d[n][B])<<endl;
}

很好,這一下時間複雜度大大下降,但是仍然沒有達到最優,還可以將三重循環減少爲兩重循環!從dp_2可以看出我們在填第i行第j列時,使用的是第i-1行、從第0列到第j列的數之和。可以由此想到前綴和,將dp_2中的t從第三重循環調整至第二重循環來計算前綴和,筆者不贅述,直接在【Cpp代碼】中給出dp函數和完整程序,時間複雜度爲這裏寫圖片描述

最後提一點,這一個題實質上是斯特林數,的運用,關於斯特林數,詳見百度百科 斯特林數

【Cpp代碼】
#include<cstdio>
#include<iostream>
#define maxn 25 
using namespace std;
int n,A,B;
unsigned long long d[maxn][maxn];

void dp_2()
{
    int limit=max(A,B);
    //狀態函數f(i,j)=在前i個盒子中放入不多於j個(紅/藍)球的方案數 
    //狀態轉移方程f(i,j)=f(i-1,j-m)(0<=m<=j)

    for(int i=0;i<=limit;i++)   d[0][i]=1;

    for(int i=1;i<=n;i++)
    {
        unsigned long long t=0;
        for(int j=0;j<=limit;j++)//第i個盒子裝j個球 
        {
            t+=d[i-1][j];
            d[i][j]=t;
        }
    }
    cout<<(d[n][A]*d[n][B])<<endl;
}

int main()
{
    scanf("%d%d%d",&n,&A,&B);
    dp();
    return 0;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章