比较数学的一场,难度稍大。
Task 1:数数
【问题描述】
fadbec 很善于数数,⽐如他会数将a 个红球,b 个黄球,c 个蓝球,d个绿球排成⼀列,求出任意相邻不同⾊的方案数⽬。
现在R 君不知道fadbec 数的对不对,想让你也算⼀算。
由于数字⽐较⼤,所以请输出除以109 + 7 的余数。
【输入格式】
⼀⾏四个正整数a,b,c,d。
【输出格式】
输出包含⼀个整数,表⽰答案。
【样例输入1】
1 1 1 2
【样例输出1】
36
【数据规模及约定】
对于前30% 的数据,1 <=a; b; c; d <= 3。
对于前100% 的数据,1 <= a; b; c; d <= 30。
直观想法:暴力搜索,判断不同,有30分。
稍加改造:只有四种球,取s个数的时候对应的有多种不同状态,按状态转移进行DP,注意空间和时间优化,100pts
比较简单,不做过多解释。
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
#define ns s&1
#define ls ~s&1
#define lint long long
using namespace std;
const lint modd=1000000007;
lint a,b,c,d,f[2][31][31][31][5];
inline lint mod(lint x){
return x%modd;
}
inline lint upd(lint s,lint i,lint j,lint k,lint p){
lint sum=0;
for(int pp=0;pp<=4;++pp){
if(pp==p)continue;
sum=mod(sum+f[s][i][j][k][pp]);
}
return mod(sum);
}
int main(){
freopen("count.in","r",stdin);
freopen("count.out","w",stdout);
scanf("%lld%lld%lld%lld",&a,&b,&c,&d);
f[0][0][0][0][0]=1;
lint sum=a+b+c+d;
for(int s=1;s<=sum;++s){
memset(f[ns],0,sizeof(f[ns]));
lint maxi=min(a,sum);
for(int i=0;i<=maxi;++i){
lint maxj=min(b,sum-i);
for(int j=0;j<=maxj;++j){
lint maxk=min(c,sum-i-j);
for(int k=0;k<=maxk;++k){
lint l=s-i-j-k;
if(l<0||l>d)continue;
// for(int p=1;p<=4;++p){
if(i>0)f[ns][i][j][k][1]=mod(f[ns][i][j][k][1]+upd(ls,i-1,j,k,1));
if(j>0)f[ns][i][j][k][2]=mod(f[ns][i][j][k][2]+upd(ls,i,j-1,k,2));
if(k>0)f[ns][i][j][k][3]=mod(f[ns][i][j][k][3]+upd(ls,i,j,k-1,3));
if(l>0)f[ns][i][j][k][4]=mod(f[ns][i][j][k][4]+upd(ls,i,j,k-0,4));
// }
// printf("s=%lld ns=%lld a=%lld b=%lld c=%lld %lld %lld %lld %lld\n",s,ns,i,j,k,f[ns][i][j][k][1],f[ns][i][j][k][2],f[ns][i][j][k][3],f[ns][i][j][k][4]);
}
}
}
}
lint ans=mod(mod(f[sum&1][a][b][c][1]+f[sum&1][a][b][c][2])+mod(f[sum&1][a][b][c][3]+f[sum&1][a][b][c][4]));
printf("%lld\n",ans);
return 0;
}
Task 2:数组
【问题描述】
fabdec 有⼀个长度为n 的数组a[](下标1-n), 初始时都是0。
fabdec 随机了⼀个1 到n 的随机数x,并且把a[x]++。
fabdec 重复了m 次这样的操作,然后数了⼀下数组⾥⼀共有k 个位置为奇数。
fabdec 现在想问执⾏m 次操作,总共能⽣成多少种不同的数组使得恰好有k 个位置是奇数?(两个数组不同当且仅当两个数组存在某个位置数组的值不相同)
因为这个数字会很⼤,所以只需输出这个答案除以109 + 7 的余数。
【输入格式】
⼀⾏三个整数,n,m,k。
【输出格式】
输出包含⼀个整数,表⽰答案。
【样例输入】
2 3 1
【样例输出】
4
【数据规模及约定】
对于前20% 的数据,1 <= n;m <= 4。
对于前50% 的数据,1 <= n;m <= 2000。
对于前100% 的数据,1 <= n;m <= 100000, 0 <= k <= n。
考虑方法:
直接硬搞搜索,20pts
直接DP,50pts
正解的想法比较巧妙:
奇数的本质其实就是二进制位下最低位为1,所以k位位奇数,那就在k位上添加上一个1,选择方法有C(n,k)种。剩下的数拆分成多个2的形式,随意分配到每一位上,计算分配类型总数即可。
由于数学不好,我通过打表得到结论:对于ss=(m-k)/2个2可重复地填到n位上,选择方法有C(n+ss-1,ss)种。
- 根据乘法原理,答案就是C(n,k)*C(n+ss-1,ss),注意要大力取模。
- 考虑组合数的计算:因为模数是质数,而且n范围比较大,考虑线性求阶乘逆元。
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
#define MAXN 400010
#define lint long long
using namespace std;
const lint mod=1000000007;
lint n,m,k,fac[MAXN],inv[MAXN];
//put k into n positions;
inline lint __pow(lint x,lint y){//x^y
lint s=1;
while(y!=0){
if(y&1){
y^=1;
s=x*s%mod;
}
y>>=1;
x=x*x%mod;
}
return s;
}
void get_fac(lint maxn){
fac[0]=1;
for(lint i=1;i<=maxn;++i){
fac[i]=i*fac[i-1]%mod;
}
inv[maxn]=__pow(fac[maxn],mod-2);
// printf("inv=%lld\n",inv[n+k]);
for(lint i=maxn;i>=2;--i){
inv[i-1]=inv[i]*i%mod;
}
}
inline lint C(lint x,lint y){
return ((fac[x]*inv[y])%mod)*inv[x-y]%mod;
}
int main(){
freopen("array.in","r",stdin);
freopen("array.out","w",stdout);
scanf("%lld%lld%lld",&n,&m,&k);
if((m-k)&1){
puts("0");
return 0;
}
lint ss=(m-k)>>1;
lint maxn=max(max(n+ss-1,ss),max(n,k));
get_fac(maxn);
// for(int i=1;i<=n+k;++i)printf("%d ",fac[i]);puts("");
lint ans=C(n+ss-1,ss)*C(n,k)%mod;
printf("%lld",ans);
return 0;
}
Task 3:子集【问题描述】
R 君得到了⼀个集合,⾥⾯⼀共有n 个正整数,R 君对这个集合很感兴趣,通过努⼒钻研,发现了这个集合⼀共有2n 个⼦集。
现在R 君又对这个集合的⼦集很感兴趣。
定义⼀个集合的权值是这个集合内所有数字的和的话,那么R 君想问问你,这个集合的权值第K ⼩⼦集是多⼤。
ps. 涉及到较少数字的long long 输⼊输出,建议使用cin/cout。
【输入格式】
第⼀⾏两个正整数n,k。
接下来⼀⾏n 个正整数,表⽰集合内元素。
【输出格式】
输出⼀个数字,表⽰集合的权值第K ⼩⼦集的权值。
【样例输入】
2 3
1 2
【样例输出】
2
6
【数据规模及约定】
- 对于前20% 的数据,1 <= n <= 15。
- 对于前40% 的数据,1 <= n <= 22。
- 对于前100% 的数据,1 <= n <=35, 1 <= k <= 2^n,1 <= 集合元素<=1000000000
朴素算法直接dfs构造所有子集,排序求解即可,40pts。
正解需要使用meet-in-the-middle的思想。正向搜索搜索树的大小是235的,是不可接受的。如果两端同时开始搜索,就可以让搜索树大小变成216+215。但是对于中间合并的时候要仔细考虑,避免复杂度退化成216^2。所以想到双向搜索后,本题的核心问题就成为了怎么把分开的两个小集合最终合并成一个大集合。
这里我们考虑二分答案,二分第k大集合的数值,判断该数值不小于的集合数与k的比较。其中判断函数中的方法类似于“悬线法”的思想,把子集排序后用两个指针来记录答案总数。
顺带一提,预处理拆分的子集构造还有一种更容易写的方法-使用lowbit构造。但是因为本蒟蒻不会+难理解+实测更慢,在这里本蒟蒻选择搜索构造。
#include<cstdio>
#include<iostream>
#include<algorithm>
#define lint long long
using namespace std;
const int MAXN=(1<<18)+5;
lint n,k,sum,arr[40],cnt_1,cnt_2,s1[MAXN],s2[MAXN];
void dfs_1(lint pos,lint val,lint ed){
s1[++cnt_1]=val;
for(lint i=pos;i<=ed;++i){
dfs_1(i+1,val+arr[i],ed);
}
}
void dfs_2(lint pos,lint val,lint ed){
s2[++cnt_2]=val;
for(lint i=pos;i<=ed;++i){
dfs_2(i+1,val+arr[i],ed);
}
}
inline bool judge(lint x){
lint p1=1,p2=1,ans=0;
while(s1[p1+1]+s2[p2]<=x&&p1+1<=cnt_1)p1++;
// printf("p1=%lld p2=%lld\n",p1,p2);
while(p2<=cnt_2){
while(s2[p2]+s1[p1]>x&&p1>=0)p1--;
if(p1<0)break;
// printf("ans+=%d\n",p1);
ans+=p1;
p2++;
}
// printf("x=%lld ans=%lld\n",x,ans);
return ans>=k;
}
int main(){
freopen("subset.in","r",stdin);
freopen("subset.out","w",stdout);
cin>>n>>k;
for(int i=1;i<=n;++i){
cin>>arr[i];
sum+=arr[i];
}
lint bg_1=1,bg_2=n/2+1;
lint ed_1=n/2,ed_2=n;
dfs_1(bg_1,0,ed_1);
dfs_2(bg_2,0,ed_2);
/*
思路:
- 把集合分成两半
- 预处理这两个集合的所有子集
- 二分答案
*/
sort(s1+1,s1+1+cnt_1);
sort(s2+1,s2+1+cnt_2);
lint l=1,r=sum;
while(r>l+1){
lint mid=(l+r)>>1;
if(judge(mid)){
r=mid;
}else{
l=mid;
}
}
printf("%lld",r);
}