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;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章