题目
一个k行,每行n[i]个数字的容器。给出Σn[i]个数字,填入该容器,使得每一行递减,每一列递减。求方案数。(k<=5,Σn[i]<=30)
思考
一般人会去思考,给第(i,j)填x的方案数。这样问题就很复杂了,因为当填(i,j)时,要考虑(i-1,j)和(i,j-1)两个格子的数字,而DP又不便记录。
我们换一种想法,先说说一种朴素做法。记目前每行已填了(a[1],a[2],a[3],a[4],a[5])个数。现在,从大到小要填入一个数x,我们可以给它填在(1,a[1]+1)(前提a[1]+1<=n[i]),或者填在(i,a[i]+1)(i>1)(前提a[i]+1<=n[i]且a[i]+1<=a[i-1])。这样填能保证最终有一个合法答案。
进而,我们可以忽略现在填数是什么,因为只要按大到小的顺序填下去,总是合法的,而且还一定能产生一些贡献。或者理解成我们现在填的数是第Σa[i]+1大的数。
题解
DP
在上述朴素做法下,我们将其转为DP的形式,累计方案数。
设f[a][b][c][d][e]表示每行已填了(a[1],a[2],a[3],a[4],a[5])个数的方案数。由于f[30][30][30][30][30]会MLE,所以要动态开数组。
初始化:f[0][0][0][0][0]=1
转移方程:在上述朴素做法的条件上,可以有(a[1],a[2],a[3],a[4],a[5])转移到(a[1]+1,a[2],a[3],a[4],a[5]),(a[1],a[2]+1,a[3],a[4],a[5])…(a[1],a[2],a[3],a[4],a[5]+1)。
代码
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
int k,n[10],a[10];
int main()
{
while(scanf("%d",&k),k!=0)
{
memset(n,0,sizeof(n));
for(int i=1;i<=k;i++) scanf("%d",&n[i]);
unsigned f[n[1]+1][n[2]+1][n[3]+1][n[4]+1][n[5]+1];
memset(f,0,sizeof(f));f[0][0][0][0][0]=1;
for(a[1]=0; a[1]<=n[1]; a[1]++)
for(a[2]=0; a[2]<=n[2]; a[2]++)
for(a[3]=0; a[3]<=n[3]; a[3]++)
for(a[4]=0; a[4]<=n[4]; a[4]++)
for(a[5]=0; a[5]<=n[5]; a[5]++)
{
int tmp=f[a[1]][a[2]][a[3]][a[4]][a[5]];
if(a[1]<n[1]) f[a[1]+1][a[2]][a[3]][a[4]][a[5]]+=tmp;
if(a[2]<n[2] && a[2]<a[1]) f[a[1]][a[2]+1][a[3]][a[4]][a[5]]+=tmp;
if(a[3]<n[3] && a[3]<a[2]) f[a[1]][a[2]][a[3]+1][a[4]][a[5]]+=tmp;
if(a[4]<n[4] && a[4]<a[3]) f[a[1]][a[2]][a[3]][a[4]+1][a[5]]+=tmp;
if(a[5]<n[5] && a[5]<a[4]) f[a[1]][a[2]][a[3]][a[4]][a[5]+1]+=tmp;
}
printf("%u\n",f[n[1]][n[2]][n[3]][n[4]][n[5]]);
}
return 0;
}