[递推] BZOJ1037: [ZJOI2008]生日聚会Party

本文介绍了一种利用递推思想解决特定计数问题的方法。问题要求计算在一定条件下,排列序列中男孩和女孩数量差不超过指定值的所有可能方案数。通过定义状态转移方程,文章详细阐述了如何高效地计算出所有合法排列的数量。
摘要由CSDN通过智能技术生成
题意

有n个男孩和m个女孩,排成一个n+m的序列,满足对于任意连续的一段,男孩与女孩的数目之差不超过a。求方案数。
n,m ≤ 150,k ≤ 20

题解

肯定朝递推的方向思考。如果序列前i个已经合法,现在多填入一个i+1,设男孩为1,女孩为-1,那么只需要考虑所有后缀和的绝对值是否小于等于a即可。
我们的关注点就在所有的后缀和上。每次填入一个数,所有后缀和都将+1/-1,且多生成了一个1/-1的后缀和。判断是否满足绝对值小于等于a,我们是不是只需要记下正数的最大值以及负数的绝对值的最大值就行了呢?
设k1为正数最大值,k2为负数绝对值最大值,当新来了一个1时,k1+1,k2-1,当新来了-1时,k1-1,k2+1。
状态的定义已经渐渐清晰了:f[i][j][k1][k2]表示已经填了i个1,j个-1,k1k2的含义同上,这样一个状态的方案数。
好像还是有点小问题:如果有后缀和在过程中符号变了会发生什么呢?因为所有后缀和都是同时增减的,大小关系不会变,所以被两个最值夹在中间的数肯定是没用的。而如果正数或负数都没有了也没关系,因为每次会多生成了一个1/-1,他会成为最值。

#include<cstdio>
#include<algorithm>
using namespace std;
const int MOD=12345678;
int n,m,K,ans,f[155][155][25][25];
int main(){
    freopen("bzoj1037.in","r",stdin);
    freopen("bzoj1037.out","w",stdout);
    scanf("%d%d%d",&n,&m,&K);
    if(K<1||!(n+m)){ printf("0\n"); return 0; }
    if(!n){
        if(m<=K) printf("1\n"); else printf("0\n");
        return 0;
    }
    if(!m){
        if(n<=K) printf("1\n"); else printf("0\n");
        return 0;
    }
    f[0][0][0][0]=1;  //n个1 m个-1 
    for(int j1=0;j1<=n;j1++) 
     for(int j2=0;j2<=m;j2++) //j1个 1  j2个 -1 
      for(int k1=0;k1<=K;k1++) //最大值 k1  
       for(int k2=0;k2<=K;k2++){ //最小值 -k2
            if(!f[j1][j2][k1][k2]) continue;
            int f_now=f[j1][j2][k1][k2];
            if(j1<n&&k1<K) f[j1+1][j2][k1+1][max(0,k2-1)]+=f_now, f[j1+1][j2][k1+1][max(0,k2-1)]%=MOD;// +1
            if(j2<m&&k2<K) f[j1][j2+1][max(0,k1-1)][k2+1]+=f_now, f[j1][j2+1][max(0,k1-1)][k2+1]%=MOD; // -1
        }
     for(int k1=0;k1<=K;k1++) 
      for(int k2=0;k2<=K;k2++)
       ans=(ans+f[n][m][k1][k2])%MOD;
    printf("%d\n",ans);
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值