JZOJ 5436. 【NOIP2017提高A组集训10.30】Group

题目

给定n个数,将这n个数分为若干组,求每一组最大值与最小值的差的和不超过K的方案数。

题解

暴力可以过 N10 的数据。
N50 ,设 f[i][j][k] 表示做到i,分了j组,每一组最大值与最小值的差的和为k的方案数。但是这样k的范围很大,会爆。
这时,一个有序的序列会比一个无序的序列更容易被想到做法。
此时有序胜无序
题目的条件:
①只有每组的最大/最小值会对答案有贡献。
②最大值与最小值的差的和受到了不超过K的限制。
如果去掉条件②那很好做,就是斯特林数。
f[i][j] 表示做到i分了j组。 f[i][j]=f[i1][j]j+f[i1][j1]
加上了条件②,我们第二维的设法可能要改一下。
如果第二维只表示分了j组,在不知道每组的最大值/最小值的情况下很难去设第三维。这是最困惑人的地方。
由于不能直接确定每组的最大值和最小值导致 Σ ,所以考虑当前的 Σ ,即此时的 Σ 不代表当前i个元素最后所属集合的每一组最大值与最小值的差的和。

表示不了目前确切的 Σ ,可以尝试表示一下暂时匹配完+没匹配完的 Σ
f[i][j][k] 表示做到i,还有j组没有分完,暂时的 Σ =k的方案数。
分两种情况。
①新开一组。
f[i][j+1][k+(a[i]a[i1])j]+=f[i1][j][k] (元素a[i]不是新组的max)
f[i][j][k+(a[i]a[i1])j]+=f[i1][j][k] (元素a[i]是新组的max)
②继续把新元素分入旧组。
f[i][j][k+(a[i]a[i1])]+=f[i1][j][k]j (元素a[i]不是旧组的max)
f[i][j1][k+(a[i]a[i1])]+=f[i1][j][k]j (元素a[i]是旧组的max)
用滚动数组就可以将空间控制在范围之内。

正确性

由于序列的无序性,我们排下序,不管是有没有匹配完的组的max-min,设min的位置是j1,max的位置是j2,那么这一组对答案的贡献为

a[j1+1]a[j1]+a[j1+2]a[j1+1]+...+a[j2]a[j21]=a[j2]a[j1]

总结

①对于DP题目明确题目关键条件(老梗不继续吹)
②若有困惑你的条件,试想将这个条件去掉怎么做,然后再加上这个条件,分类讨论的情况大概跟去掉这个条件后分类讨论的情况差不多。
DP不一定要表现形象的东西(这也是老梗),一个数看看能否通过拆散(这种老套路的题目很多啊)从而有利于DP转移。
④如果序列有无序性,那么此时可以排排序。

代码

#include<iostream>
#include<cstdio>
#include<cmath>
#include<cstring>
#include<algorithm>
#define N 210
#define LL long long
#define mo 1000000007
#define fo(i,a,b) for(i=a;i<=b;i++)
using namespace std;
LL i,j,k,l,v,n,m,ans,tmp,o;
LL a[N];
LL f[2][N][N*5];
bool cmp(LL x,LL y){return x<y;}
void add(LL &x,LL y){x=(x+y)%mo;}
int main(){
    scanf("%lld%lld",&n,&m);
    fo(i,1,n)scanf("%lld",&a[i]);
    sort(a+1,a+n+1,cmp);
    o=0;
    f[o][0][0]=1;
    fo(i,1,n){
        o=1^o;
        memset(f[o],0,sizeof(f[o]));
        fo(j,0,i-1){
            tmp=j*(a[i]-a[i-1]);
            fo(k,0,m){
                if(tmp+k>m)break;
                v=tmp+k;
                add(f[o][j+1][v],f[o^1][j][k]);
                add(f[o][j][v],f[o^1][j][k]); 
                if(j){
                    add(f[o][j][v],(f[o^1][j][k]*j)%mo);
                    add(f[o][j-1][v],(f[o^1][j][k]*j)%mo);
                }
            }
        }
    }
    fo(i,0,m)ans=(ans+f[o][0][i])%mo;
    printf("%lld\n",ans);
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值