组合数学基础练习


(这四个题目难度并列,相差不大)

T1 DiceGames

题目

TopCoder - 7601 DiceGames

描述
你有 n n n个骰子,并打算用这些骰子发明一个新游戏。为了发明游戏,你需要首先知道扔骰子有多少种结果。我们定义骰子上显示的值集合为一种结果,而不考虑顺序。因此, { 1 , 1 , 2 } \{1,1,2\} {1,1,2} { 1 , 2 , 1 } \{1,2,1\} {1,2,1} { 2 , 1 , 1 } \{2,1,1\} {2,1,1}都被视为相同的扔骰子结果,而 { 1 , 1 , 2 } \{1,1,2\} {1,1,2} { 1 , 2 , 2 } \{1,2,2\} {1,2,2} { 1 , 1 , 3 } \{ 1,1,3\} {1,1,3}都是不同的结果。请注意,即使两个骰子可能具有不同数量的面的数量,但在这个游戏中,我们只考虑骰子显示的值。给定一个int数组 s i d e s sides sides,数组中的第 i i i个数 s i d e s i sides_i sidesi表示第 i i i个骰子有多少个面。一个有 n n n面的骰子能够显示 [ 1 , n ] [1,n] [1,n]的数。求一共有多少种扔骰子结果。

数据范围
骰子的数量 n n n满足 1 ≤ n ≤ 32 1≤n≤32 1n32
每个骰子的面数 s i d e s i sides_i sidesi满足 1 ≤ s i d e s i ≤ 32 1≤sides_i≤32 1sidesi32

输入格式
1 1 1行: 1 1 1个整数 n n n
2 2 2行: n n n个正整数

输出格式
1 1 1行: 1 1 1个整数,表示答案

输入样例 1

1 1 1
4

输出样例 1

4

输入样例 2

2
4 4

输出样例 2

10

分析

考试的时候想用背包(母函数)做,即统计每个点数出现的次数 c n t [ i ] cnt[i] cnt[i],最后从每个 c n t [ i ] cnt[i] cnt[i]中选一些,使其和为 n n n,写的时候就发现有问题(・`ω´・),但是没举出反例,其实反例很简单:

4
3 4 5 6

这样统计方案时会统计到1 5 5 6等,但是能提供56的只有两个骰子,不可能出现3个56


正解是先排序,然后DP时保证后来的不比先来的小,即可找到所有的方案且不重复,因为大的骰子一定可以投出小骰子的点数,但如果大骰子都不能投出,小骰子在后面就一定也不能投出这个点数。
就这样了,我太弱了= =

代码

#include<set>
#include<map>
#include<queue>
#include<cmath>
#include<cstdio>
#include<vector>
#include<cstring>
#include<iostream>
#include<algorithm>
#include<functional>
using namespace std;

int read(){
    int x=0,f=1;char c=getchar();
    while(c<'0'||c>'9'){if(c=='-')f=-1;c=getchar();}
    while(c>='0'&&c<='9')x=(x<<3)+(x<<1)+(c^48),c=getchar();
    return x*f;
}

#define MAXN 32
int N;
int A[MAXN+5];
long long dp[MAXN+5][MAXN+5];
//前i个骰子 最后一个投了j点的方案数

int main(){
    freopen("dicegames.in" ,"r", stdin);
    freopen("dicegames.out","w",stdout);
    N=read();
    for(int i=1;i<=N;i++)
        A[i]=read();
    sort(A+1,A+N+1);
    dp[0][0]=1;
    for(int i=1;i<=N;i++)
        for(int j=1;j<=A[i];j++)
            for(int k=0;k<=j;k++)
                dp[i][j]+=dp[i-1][k];
    long long Ans=0;
    for(int i=1;i<=MAXN;i++)
        Ans+=dp[N][i];
    printf("%lld",Ans);
}

T2 WordNumber

题目

TopCoder - 3488 WordNumber

描述
用字母表中的前 α \alpha α个小写字母构造单词,把所有能构造出的单词按照以下排序规则排序:

  • 如果两个单词长度不同,那么长度更短的会排在前面
  • 如果两个单词长度相同,那么字典序更小的会排在前面

例如,如果我们仅使用前 3 3 3个字母abc,那么这些单词就会被这样排序:abcaaabacbabb……其中a是第 1 1 1个单词,bb 是第 8 8 8个单词。求按照这个排序规则,第 n n n个单词是什么。

数据范围
可以使用的小写字母的种类数 α \alpha α满足 2 ≤ α ≤ 26 2≤\alpha≤26 2α26
所求单词的序号 n n n满足 1 ≤ n ≤ 2 × 1 0 9 1≤n≤2\times10^9 1n2×109

输入格式
第 1 行:1 个整数 alpha
第 2 行:1 个整数 n

输出格式
第 1 行:1 个字符串

输入样例 1

3
5

输出样例 1

ab

输入样例 2

26
2000000000

输出样例 2

flhomvx

分析

还是凑数游戏啊啊啊,考试的时候题意理解的有问题直接 α \alpha α进制凑数结果居然过给的了 7 7 7个样例,最后跑出来居然总共过了 25 25 25(共 62 62 62)个样例,,,,,,,,,


先是等比数列找出长度,然后除和模的时候都很玄妙(+1 -1要仔细想临界状态才知道),多调试,这种凑数题我真的找不出什么适用的办法,我太弱了= =

其实对于第 d d d d = 2 k d=2k d=2k)层,涂两种颜色的排列数是 C d k C_{d}^{k} Cdk,对于第 d d d d = 3 k d=3k d=3k层),涂三种颜色的排列数是 C d k × C d − k k C_d^k\times C_{d-k}^k Cdk×Cdkk,,,,,,

代码

#include<set>
#include<map>
#include<queue>
#include<cmath>
#include<cstdio>
#include<vector>
#include<cstring>
#include<iostream>
#include<algorithm>
#include<functional>
using namespace std;

int read(){
    int x=0,f=1;char c=getchar();
    while(c<'0'||c>'9'){if(c=='-')f=-1;c=getchar();}
    while(c>='0'&&c<='9')x=(x<<3)+(x<<1)+(c^48),c=getchar();
    return x*f;
}

long long A,N;
long long Pow(long long x,int y){
    long long ret=1;
    while(y){
        if(y&1)
            ret*=x;
        x*=x;
        y>>=1;
    }
    return ret;
}

int main(){
    freopen("wordnumber.in" ,"r", stdin);
    freopen("wordnumber.out","w",stdout);
    A=read(),N=read();
    int len=1;
    while(Pow(A,len)<N)
        N-=Pow(A,len++);
    while(len){
        putchar('a'+(N-1)/Pow(A,--len));
        N-=(N-1)-(N-1)%Pow(A,len);//这两句要自己摸索很久才打得出来
    }
    return 0;
}

T3 ChristmasTree

题目

TopCoder - 7261 ChristmasTree

描述
你正在装饰你的圣诞树。树有 n n n层,将层数从上到下编号为 1 1 1 n n n。你有许多红、绿、蓝这三种颜色的装饰品,你决定以下面的方式挂起它们:在第 k k k层,你将挂起一排正好 k k k个装饰品。对于某一层的装饰品用到的颜色,在这一层中这几种颜色的装饰品数量相等。例如,我们用R代表红色装饰品,G代表绿色装饰品,B代表蓝色装饰品,考虑以下两棵树:
示例
左边的树符合我们的要求,但是右边的树不符合,因为第三层有 1 1 1个蓝色装饰品但是有 2 2 2个红色装饰品。已知红、绿、蓝三种颜色的装饰品分别有 r e d red red g r e e n green green b l u e blue blue个,求有多少种符合要求的装饰圣诞树的方法。对于两种圣诞树,如果树上某一个位置挂的装饰品颜色不同,那么认为是两种装饰方法。

数据范围
树的层数 n n n满足 1 ≤ n ≤ 10 1≤n≤10 1n10
红色装饰品的数量 r e d red red满足 0 ≤ r e d ≤ 50 0≤red≤50 0red50
绿色装饰品的数量 g r e e n green green满足 0 ≤ g r e e n ≤ 50 0≤green≤50 0green50
蓝色装饰品的数量 b l u e blue blue满足 0 ≤ b l u e ≤ 50 0≤blue≤50 0blue50

输入格式
1 1 1行: 1 1 1个整数 n n n
2 2 2行: 1 1 1个整数 r e d red red
3 3 3行: 1 1 1个整数 g r e e n green green
4 4 4行: 1 1 1个整数 b l u e blue blue

输出格式
1 1 1行: 1 1 1个整数

输入样例 1

2
1
1
1

输出样例 1

6

输入样例 2

8
1
15
20

输出样例 2

197121

分析

显然对于第 d d d层:

  • 可全涂一种颜色
  • d = 2 k d=2k d=2k,可涂两种颜色,每种 k k k(顺序可以有多种)
  • d = 3 k d=3k d=3k,可涂三种颜色,每种 k k k(顺序可以有多种)

记忆化搜索即可。
考试的时候做到这,花了10分钟写完记忆化搜索发现过不了第二个样例,想了几万年,才发现顺序可以有多种,于是又想了半天不知道怎么算,最后用next_permutation打表才过了o(`ω´*)o我还是太弱了= =

代码

#include<set>
#include<map>
#include<queue>
#include<cmath>
#include<cstdio>
#include<vector>
#include<cstring>
#include<iostream>
#include<algorithm>
#include<functional>
using namespace std;

int read(){
    int x=0,f=1;char c=getchar();
    while(c<'0'||c>'9'){if(c=='-')f=-1;c=getchar();}
    while(c>='0'&&c<='9')x=(x<<3)+(x<<1)+(c^48),c=getchar();
    return x*f;
}

#define MAXN 10
#define MAXM 50
int N,R,G,B;
long long fac[MAXN+5];
long long dp[MAXN+2][MAXM+2][MAXM+2][MAXM+2];

const long long Table2[10]={0,6,90,1680};
const long long Table1[10]={0,2,6,20,70,252};
//打的表
//例如Table1[2]是1122的排列数,Table2[3]是111222333的排列数
long long dfs(int d,int r,int g,int b){
    if(r<0||g<0||b<0)
        return 0;
    long long &ret=dp[d][r][g][b];
    if(ret!=-1)
        return ret;
    if(d==N+1)
        return ret=1;
    ret=dfs(d+1,r-d,g,b)+dfs(d+1,r,g-d,b)+dfs(d+1,r,g,b-d);
    //涂一种颜色
    if(d%2==0)
        ret+=(dfs(d+1,r-d/2,g-d/2,b)+dfs(d+1,r-d/2,g,b-d/2)+dfs(d+1,r,g-d/2,b-d/2))*Table1[d/2];
        //涂两种颜色
    if(d%3==0)
        ret+=dfs(d+1,r-d/3,g-d/3,b-d/3)*Table2[d/3];
        //涂三种颜色
    return ret;
}

int main(){
    freopen("christmastree.in" ,"r", stdin);
    freopen("christmastree.out","w",stdout);
    N=read(),R=read(),G=read(),B=read();
    memset(dp,-1,sizeof dp);
    printf("%lld",dfs(1,R,G,B));
}

T4 MuddyRoad2

题目

TopCoder - 12189 MuddyRoad2

描述
n n n个点,序号分别从 0 0 0 n − 1 n-1 n1。点 i i i和点 i + 1 i+1 i+1之间有一条边。现在你要从点 0 0 0前往点 N − 1 N-1 N1。你每次可以向前走一步或两步,即如果你在点 i i i,那么你可以走到点 i + 1 i+1 i+1或走到 i + 2 i+2 i+2(不用经过 i + 1 i+1 i+1)已知 0 0 0号点和 N − 1 N-1 N1号点是安全的,其他的 n − 2 n-2 n2个点中有恰好 m u d d y C o u n t muddyCount muddyCount个点是不安全的,你不能经过不安全的点。从 0 0 0号点出发到达 n − 1 n-1 n1号点有多种走法。如果两种走法某一步的长度不同,那么被视为两种走法。求这 m u d d y C o u n t muddyCount muddyCount个点有多少种情况使得走法有偶数种,对 555555555 555555555 555555555取模。两种情况中,如果某个点的安全状态不一样,那么这两种状况被视为不同的状况。

数据范围
点的个数 n n n满足 2 ≤ n ≤ 555 2≤n≤555 2n555
不安全的点的个数 m u d d y C o u n t muddyCount muddyCount满足 0 ≤ m u d d y C o u n t ≤ n − 2 0≤muddyCount≤n-2 0muddyCountn2

输入格式
1 1 1行: 1 1 1个整数 n n n
2 2 2行: 1 1 1个整数 m u d d y C o u n t muddyCount muddyCount

输出格式
1 1 1行: 1 1 1个整数

输入样例 1

5
2

输出样例 1

2

输入样例 2

314
78

输出样例 2

498142902

分析

显然,如果第 i i i格有石头,那你必须从 i − 1 i-1 i1跳到 i + 1 i+1 i+1,只有一种走法。所以对于一种石头的放法,走法的数量就是石头隔开的每段平路的走法的数量之积,也就是说,只要有一段平路的走法数是偶数,这种放石头的方法就是符合要求的。

对于一段平路,走法是多少?由于一次可以跳一格或两格,所以有递推式 f [ i ] = f [ i − 1 ] + f [ i − 2 ] f[i]=f[i-1]+f[i-2] f[i]=f[i1]+f[i2],这是斐波拉契数列,根据“奇+奇=偶”“奇+偶=偶”,斐波拉契数列的奇偶:奇、奇、偶、奇、奇、偶……也就是说有 3 3 3的倍数格时,有偶数种走法。

然后就可以DP了:
(为了方便,格子编号 1 1 1 n n n,在 n + 1 n+1 n+1处放一块石头,一共放 m + 1 m+1 m+1个石头)
d p [ i ] [ j ] [ 0 ] dp[i][j][0] dp[i][j][0]表示跳到第 i − 1 i-1 i1个点, 1 1 1 i i i格中放了 j j j个石头(第 i i i格必放), 前面还没有区间是偶数种走法的方案数; d p [ i ] [ j ] [ 1 ] dp[i][j][1] dp[i][j][1]表示跳到第 i − 1 i-1 i1个点, 1 1 1 i i i格中放了 j j j个石头(第 i i i格必放), 前面已经有区间是偶数种走法的方案数,就有转移:
d p [ i ] [ j ] [ 0 ] = ∑ k = 0 i − 1 { d p [ k ] [ j − 1 ] [ 0 ] ( 从 k + 1 跳 到 i − 1 有 奇 数 种 走 法 ) 0 ( 从 k + 1 跳 到 i − 1 有 偶 数 种 走 法 ) dp[i][j][0]=\sum\limits_{k=0}^{i-1} \begin{cases} dp[k][j-1][0] &amp; (从k+1跳到i-1有奇数种走法)\\ 0 &amp; (从k+1跳到i-1有偶数种走法) \end{cases} dp[i][j][0]=k=0i1{dp[k][j1][0]0(k+1i1)(k+1i1)
d p [ i ] [ j ] [ 1 ] = ∑ k = 0 i − 1 { d p [ k ] [ j − 1 ] [ 1 ] ( 从 k + 1 跳 到 i − 1 有 奇 数 种 走 法 ) d p [ k ] [ j − 1 ] [ 1 ] + d p [ k ] [ j − 1 ] [ 0 ] ( 从 k + 1 跳 到 i − 1 有 偶 数 种 走 法 ) dp[i][j][1]=\sum\limits_{k=0}^{i-1} \begin{cases} dp[k][j-1][1] &amp; (从k+1跳到i-1有奇数种走法)\\ dp[k][j-1][1]+dp[k][j-1][0] &amp; (从k+1跳到i-1有偶数种走法) \end{cases} dp[i][j][1]=k=0i1{dp[k][j1][1]dp[k][j1][1]+dp[k][j1][0](k+1i1)(k+1i1)

但是这样的话,即使利用前面的斐波拉契数列奇偶性判断也是 O ( n 3 ) O(n^3) O(n3),会T

由于当 i − 1 − k ≡ 0 m o d &ThinSpace;&ThinSpace; 3 i-1-k\equiv0\mod3 i1k0mod3时从 k + 1 k+1 k+1跳到 i − 1 i-1 i1有偶数种跳法,移项得 i − 1 ≡ k m o d &ThinSpace;&ThinSpace; 3 i-1\equiv k\mod3 i1kmod3时从 k + 1 k+1 k+1跳到 i − 1 i-1 i1有偶数种跳法,于是以除以 3 3 3的余数作为下标,前缀和优化即可。

考试的时候都没有考虑过这道题,我太弱了= =

代码

这是无优化(开O2后)大约过 3 4 \dfrac{3}{4} 43点的DP:

#include<set>
#include<map>
#include<queue>
#include<cmath>
#include<cstdio>
#include<vector>
#include<cstring>
#include<iostream>
#include<algorithm>
#include<functional>
using namespace std;

int read(){
    int x=0,f=1;char c=getchar();
    while(c<'0'||c>'9'){if(c=='-')f=-1;c=getchar();}
    while(c>='0'&&c<='9')x=(x<<3)+(x<<1)+(c^48),c=getchar();
    return x*f;
}

#define MAXN 555
#define MOD 555555555
int N,M;
long long dp[MAXN+5][MAXN+5][2];
//dp[i][j][1/0] 跳到第i-1个点 放j个石头(第i个点必放) 前面有/没有区间是偶数种方法 的方案数

void Add(long long &A,long long B){
    A=(A+B)%MOD;
}

int main(){
    freopen("muddyroad2.in" ,"r", stdin);
    freopen("muddyroad2.out","w",stdout);
    N=read(),M=read();
    dp[0][0][0]=1;
    //1和N不能放 N+1必放(一共放M+1个)
    for(int i=1;i<=N+1;i++){
        if(i==1||i==N)
            continue;
        for(int j=1;j<=min(M+1,i-1);j++){
            for(int k=0;k<i;k++){
                if((i-k)%3==1)
                    Add(dp[i][j][1],dp[k][j-1][0]);
                else
                    Add(dp[i][j][0],dp[k][j-1][0]);
                Add(dp[i][j][1],dp[k][j-1][1]);
            }
        }
    }
    printf("%lld",dp[N+1][M+1][1]);
}

这是前缀和优化过的DP:

#include<set>
#include<map>
#include<queue>
#include<cmath>
#include<cstdio>
#include<vector>
#include<cstring>
#include<iostream>
#include<algorithm>
#include<functional>
using namespace std;

int read(){
    int x=0,f=1;char c=getchar();
    while(c<'0'||c>'9'){if(c=='-')f=-1;c=getchar();}
    while(c>='0'&&c<='9')x=(x<<3)+(x<<1)+(c^48),c=getchar();
    return x*f;
}

#define MAXN 555
#define MOD 555555555
int N,M;
long long Sum[MAXN+5][3][2];
long long dp[MAXN+5][MAXN+5][2];
//Sum[j][k][0/1] 前面的i中 (i-1)%3=k的 dp[i][j][0/1]之和
//dp[i][j][1/0] 跳到第i-1个点 放j个石头(第i个点必放) 前面有/没有区间是偶数种方法 的方案数

void Add(long long &A,long long B){
    A=(A+B)%MOD;
}

int main(){
    freopen("muddyroad2.in" ,"r", stdin);
    freopen("muddyroad2.out","w",stdout);
    N=read(),M=read();
    dp[0][0][0]=1;
    Sum[0][0][0]=1;
    //1和N不能放 N+1必放(一共放M+1个)
    for(int i=1;i<=N+1;i++){
        if(i==1||i==N)
            continue;
        for(int j=1;j<=min(M+1,i-1);j++){
            Add(dp[i][j][0],Sum[j-1][i%3][0]),
            Add(dp[i][j][0],Sum[j-1][(i+1)%3][0]);
            Add(dp[i][j][1],Sum[j-1][i%3][1]),
            Add(dp[i][j][1],Sum[j-1][(i-1)%3][0]);
            Add(dp[i][j][1],Sum[j-1][(i-1)%3][1]),
            Add(dp[i][j][1],Sum[j-1][(i+1)%3][1]);
        }
        for(int j=1;j<=min(M+1,i-1);j++){
            Add(Sum[j][i%3][0],dp[i][j][0]);
            Add(Sum[j][i%3][1],dp[i][j][1]);
        }
    }
    printf("%lld",dp[N+1][M+1][1]);
}

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值