第五关——数论:组合数学

20:44:00 你在台上唱着我的创作,布局谋篇像本悲情小说——许嵩《最佳歌手》

我的寒假,我美好的寒假啊啊啊

“其实我还蛮不想写你的,博客,可是没办法啊,谁叫我的寒假不要我了,我就只好要你了,博客”

目录

  • 鸽巢原理

  • 鸽巢原理推广

  • 杨辉三角和二项式系数

  • 容斥定理

  • 卡特兰数

  • 斯特林数

那接下来就要来看一下鸽巢原理(抽屉原理)

也不知道发现它的人是不是看着别人鸽子的窝盯半天才发现的,人家鸽子会不好意思的啦!

  • 定义:如果有n+1个鸽子要进n个鸽巢,则至少存在一个鸽巢种包含两个或更多的鸽子。

  • 例题:

    (2010问题求解3)记T为一队列,初始时为空,现有n个总和不超过32的正整数依次入队。如果无论这些数具体为何值,都能找到一种出队的方式,使得存在某个时刻队列T中的数之和恰好为9,那么n的最小值是_________。

    由题意可知bi取值范围为1-32,现将这32个数构造为集合{1,10},{2,11}, …, {8,17}, {18,27}, {19,28},…,{23,32} ,{24},{25},{26},这17个集合中的任一个集合不能包含两个或两个以上的 ,否则它们的差为9,故由鸽巢定理可得出,n的最小值为18.
  • 应用:

    在边长为1的正方形内任取5点,则其中至少有2点的距离不超过√2/2

  • 例题:

    吃糖果

    这道题是典型的鸽巢原理,可用鸽巢原理一种简单的推理方法隔板法进行分析,如果S<N-1.把S个糖果放到隔板之间,这N个隔板不够放.必然至少有两个隔板之间没有糖果,由于这两个隔板是同一种糖果,所以无解。反之则有解。

    注意开long long(尽管数据不大)

    #include <cstdio>
    #include<math.h>
    #include<algorithm>
     using namespace std;
    int main()
    {
        int i,j,n,sum,max_,t;
        scanf("%d",&t);
        {
            while(t--)
            {
                sum=max_=0;
                scanf("%d",&n);
                int *a=(int*)malloc(sizeof(int)*n);
                for(i=0;i<n;i++)
                {
                    scanf("%d",&a[i]);
                    max_=max(max_,a[i]);
                }
                
                for(i=0;i<n;i++)
                {
                    if(a[i]!=max_) sum+=a[i];
                    if(sum>=max_-1) break;
                }
                
                if(i==n) printf("No\n"); 
                else printf("Yes\n");
            }
        }
    }

鸽巢原理推广

  • 鸽巢原理的加强形式

定理: 令q1, q2, q3, ...., qn为正整数。如果将 q1, q2, q3, ...., qn - n + 1 个物体放到n个盒子中,则存在一个i,使得第i个盒子至少含有qi个物品 ;

证明: 假设将q1, q2, q3, ...., qn - n + 1个物品分别放到n个盒子里。如果每一个i (i = {1, 2, ..n}),第i个盒子中放少于qi 个物品,则所有盒子所放物品的总数不超过

    (q1 - 1) + (q2 - 1) + ... + (qn - 1) = q1 + q2 + ... + qn - n

显然,比所要放的总数少一个。因此可以确定,对某个i (i = {1, 2, .. n}),第i个盒子至少包含qi个物品。

  • Erdös-Szekeres定理

简单来说呢,就是在由n2+1n2+1个实数构成的序列中,必然含有长为n+1n+1的单调(增或减)子序列。

  • Ramsey定理

  1. 定义:对于一个给定的两个整m,n>=2,则一定存在一个最小整数r,使得用两种颜色(例如红蓝)无论给Kr的每条边如何染色,总能找到一个红色的Km或者蓝色的Kn。显然,当p>=r的时候,Kp也满足这个性质。

  2. 表示形式:r可以看做一个有关m,n的二元函数,即r(m,n)。r(3,3)=6.

  3. 性质:

    ①等价性 r(m,n)=r(n,m)

    ②r(2,n)=n k2较特殊 只有一条边 最小的kr为Kn

    ③r(m,2)=m

  4. 数表:

          

例题

问题很显然,6以及6以上的直接统计345的暴力统计就行了  

就是说ans=3的个数+4的个数+5的个数+C(n,6)C(n,7)+…………+C(n,n)=2^n-C(n,0)-C(n,1)-C(n,2)-3不合法的个数-4不合法的个数-5不合法的个数。

#include<bits/stdc++.h>
using namespace std;
const int maxn=55;
const int mod=1000000007;
int T;
int a[maxn][maxn];
int cc=1;
int m,n;
void input(){
    scanf("%d%d",&n,&m);
    for (int i=1;i<=m;i++){
        int u,v;
        scanf("%d%d",&u,&v);
        a[u][v]=a[v][u]=1;
    }
}
bool ch(int x,int y,int z){
    int t=a[x][y]+a[x][z]+a[y][z];
    if (t==0||t==3)return true;
    return false;
}
bool h(int x,int y,int z,int w){
    return (ch(x,y,z)||ch(x,y,w)||ch(x,z,w)||ch(y,z,w));
}
bool e(int a,int b,int c,int d,int e){
    return (ch(a,b,c)||ch(a,b,d)||ch(a,b,e)||ch(a,c,d)||ch(a,c,e)||ch(a,d,e)||ch(b,c,d)||ch(b,c,e)||ch(b,d,e)||ch(c,d,e));
}
void solve(){
    long long ans=1;
    for (int i=1;i<=n;i++){
        ans = ans*2;
        ans%=mod;
    }
    ans-=n,ans--;
    ans-=n*(n-1)/2;
    ans+=mod;
    ans%=mod;
    for(int i=1;i<=n;i++)
    for(int j=i+1;j<=n;j++)
    for(int k=j+1;k<=n;k++)
    if(!ch(i,j,k))ans--;
    for(int i=1;i<=n;i++)
    for(int j=i+1;j<=n;j++)
    for(int k=j+1;k<=n;k++)
    for(int l=k+1;l<=n;l++)
    if(!h(i,j,k,l))ans--;
    for(int i=1;i<=n;i++)
    for(int j=i+1;j<=n;j++)
    for(int k=j+1;k<=n;k++)
    for(int l=k+1;l<=n;l++)
    for(int p=1+l;p<=n;p++)
    if(!e(i,j,k,l,p))ans--;
    ans+=mod;
    ans%=mod;
    printf("Case #%d: %lld\n",cc++,ans);
}
int main(){
    scanf("%d",&T);
    while (T--){
        memset(a,0,sizeof(a));
        scanf("%d%d",&n,&m);
        for(int i=1;i<=m;i++){
            int u,v;
            scanf("%d%d",&u,&v);
            a[u][v]=a[v][u]=1;
        }
        solve();
    } 
    return 0;
}

这道题就是鸽巢原理的运用。

#include<bits/stdc++.h>
using namespace std;
const int maxn=1e5+10;
const int mod=1e9+7;
typedef long long ll;
ll vis[maxn], a[maxn];
int main() {
    std::ios::sync_with_stdio(false);
    ll n, m;
    while(cin>>n>>m){
        if(!n&&!m){
            break;
        }
        ll sum=0,t;
        memset(vis,0,sizeof(vis) );
        for(ll i=1;i<=m;i++)
        cin>>a[i];
        for(ll i=1;i<=m;i++)
        {
            sum+=a[i];
            t=sum%n;
            if(t==0) 
            {
                for(ll j=1;j<i;j++)
                cout<<j<<" ";
                cout<<i<<endl;
                break;
            }
            else if(vis[t])
            {
                for(ll j=vis[t]+1;j<i;j++)
                cout<<j<<" ";
                cout << i << endl;
                break;
            }
            vis[t] = i;
        }
    }
    return 0;
}

这道题用到的是Ramsey定理

#include<bits/stdc++.h>
using namespace std;
int T,n;
int main() {
    scanf("%d",&T);
    while(T--) 
    {
        scanf("%d",&n);
        int a[10][10]={0};
        for(int i=1; i<n; ++i)
        for(int j=i+1; j<=n; ++j) 
        {
            int t;
            scanf("%d",&t);
            if(t&&n<6) a[i][j]=a[j][i]=1;
        }
        if(n>=6)
        {
            puts("Bad Team!");
            continue;
        }
        int f=0;
        for(int i=1;i<=n;++i)
        for(int j=i+1;j<=n;++j)
        for(int k=j+1;k<=n;++k)
        if(a[i][j]&&a[i][k]&&a[j][k])
        {
            f=1;
            break;
        }
        if(f) puts("Bad Team!");
        else puts("Great Team!");
    }
    return 0;
}

19:24:54 只有我守着安静的沙漠,等待着花开。——华晨宇《烟火中的尘埃》

排列与组合

排列组合是组合学最基本的概念。所谓排列,就是指从给定个数的元素中取出指定个数的元素进行排序。组合则是指从给定个数的元素中仅仅取出指定个数的元素,不考虑排序。

排列的定义:从n个不同元素中,任取m(m≤n,m与n均为自然数,下同)个不同的元素按照一定的顺序排成一列,叫做从n个不同元素中取出m个元素的一个排列;从n个不同元素中取出m(m≤n)个元素的所有排列的个数,叫做从n个不同元素中取出m个元素的排列数,用符号 A(n,m)表示。

计算公式:               (此外规定0! = 1)

例:

假设有这样一个问题:现在有甲、乙、丙、丁4个小朋友,老师想要从中挑出2个小朋友排成一-列参加比赛 ,有几种排法?很容易枚举出来,有以下12种排法

甲乙,甲丁,甲丙,乙甲,乙丁,乙丙,丁甲,丁乙,丁丙,丙甲,丙乙,丙丁

组合的定义:从n个不同元素中,任取m(m≤n)个元素并成一组,叫做从n个不同元素中取出m个元素的一个组合;从n个不同元素中取出m(m≤n)个元素的所有组合的个数,叫做从n个不同元素中取出m个元素的组合数。用符号 C(n,m) 表示。

计算公式:

例:

如果老师只是想从4个小朋友中挑选2个参加比赛,并不考虑排队的顺序,那有多少种方法呢?同样可以枚举出来,有以下6种挑法:

甲乙,甲丁,甲丙,乙丁,乙丙,丁丙

22:19:37 如果这失忆变成了洪水,也对,也对。——鬼卞《失眠症》

杨辉三角和二项式系数

杨辉三角小的时候都学过,那么杨辉三角有些什么规律呢,大家也都知道,那么求杨辉三角除了递推打表还有下面这种方法:

每一行从上一行推导而来。如果编程求杨辉三角第n行的数字,可以模拟这个推导过程,逐级递

推,复杂度是O(n2)。不过,若改用数学公式计算,则可以直接得到结果,比用递推快多了

,这个公式就是(1+x)n。

观察(1+x)n的展开:

(1+x)0 = 1

(1+x)1 = 1+x

(1+x)2 = 1+2x+x2

(1+x)3 = 1+3x+3x2+x3

每一行展开的系数刚好对应杨辉三角每一行的数字。也就是说,杨辉三角可以用(1+x)n来定

义和计算。

二项式定理公式:(a+b)n=C0nan+C1nan1b++Cknankbk++Cnnbn(nN)

二项式定理通项:Tk+1=Cknankbk

19:41:02 如果放下是结局出口,我已无路可走。——孟凡明《某某》

容斥原理

在计数时,必须注意无一重复,无一遗漏。为了使重叠部分不被重复计算,人们研究出一种新的计数方法,这种方法的基本思想是:先不考虑重叠的情况,把包含于某内容中的所有对象的数目先计算出来,然后再把计数时重复计算的数目排斥出去,使得计算的结果既无遗漏又无重复,这种计数的方法称为容斥原理。 

公式:

两个集合的容斥关系公式:A∪B =|A∪B| = |A|+|B| - |A∩B |(∩:重合的部分)

三个集合的容斥关系公式:|A∪B∪C| = |A|+|B|+|C| - |A∩B| - |B∩C| - |C∩A| + |A∩B∩C|                                                       

例:

在1到1000的自然数中,能被3或5整除的数共有多少个?不能被3或5整除的数共有多少个?

分析:显然,这是一个重复计数问题(当然,如果不怕麻烦你可以分别去数3的倍数,5的倍数)。我们可以把“能被3或5整除的数”分别看成A类元素和B类元素,能“同时被3或5整除的数(15的倍数)”就是被重复计算的数,即“既是A类又是B类的元素”。求的是“A类或B类元素个数”。我们还不能直接计算,必须先求出所需条件。1000÷3=333……1,能被3整除的数有333个(想一想,这是为什么?)同理,可以求出其他的条件。

整除

Time Limits: 1000 ms Memory Limits: 131072 KB Detailed Limits

Description

给出n个数a1,a2……an,求区间[L,R]中有多少个整数不能被其中任何一个数整除。

Input

第一行三个正整数,n,L,R。

第二行n个正整数a1,a2……an

Output

一个数,即区间[L,R]中有多少个整数不能被其中任何一个数整除。

Sample Input

2 1 1000

10 15

Sample Output

867

Data Constraint

对于30%的数据,1<=n<=10,1<=L,R<=1000

对于100%的数据,1<=n<=18,1<=L,R<=10^9

这道题要用到容斥原理,每个a[i]都有选和不选两种可能,lcm的计算和容斥原理的实现可以用搜索来实现。时间复杂度为O(2^n)。

lcm的计算在代码里用的是gcd算的。这道题由于数据比较大,所以要开long long。

需要注意dfs函数中的关系式,是容斥原理的关键。

#include<bits/stdc++.h>
#define ll long long
using namespace std;
ll l,r,ans,a[21];
int n;
ll gcd(ll a,ll b)
{
    if(b==0)return a;
    gcd(b,a%b);
}
ll lcm(ll a,ll b)
{
    return (a*b)/gcd(a,b);
}
void dfs(int x,int y,long long z)
{
    if (x>n)
    {
        if (y%2==1) ans+=r/z-(l-1)/z;
        else ans-=r/z-(l-1)/z;
        return;
    }
    dfs(x+1,y+1,lcm(a[x],z));
    dfs(x+1,y,z);
}
int main()
{
    scanf("%d%lld%lld",&n,&l,&r);
    for (int i=1;i<=n;i++)
    scanf("%lld",&a[i]);
    dfs(1,1,1);
    printf("%lld",ans);
    return 0;
}

给n*m个点(1 ≤ m, n ≤ 1e5),左下角的点为(1,1),右上角的点(n,m),一个人站在(0,0)看这些点。在一条直线上,只能看到最前面的一个点,后面的被档住看不到,求这个人能看到多少个点。

在同一条直线(y = kx (k为自然数))上的点只能看见最前面的 最前面的点的 y 和 x 肯定互质

所以就变成了 求m * n 这个区域中互质的 x 与 y 的对数

对于每一个1 ~ n 求 1 ~ m中有多少个与之互质的数  加起来就好了

#include<bits/stdc++.h>
#define ll long long
const int N=100010
int p[N];
int q[N];
int k;
void getp(int n)
{
    int i,j;
    k=0;
    for(i=2;i*i<=n;i++)
    {
        if(n%i==0)
        {
            p[k++]=i;
            while(n%i==0)
                n/=i;
        }
    }
    if(n>1)
        p[k++]=n;
}
int solve(int n)
{
    int i,j,kk,t=0;
    ll sum=0;
    q[t++]=-1;
    for(i=0;i<k;i++)
    {
        kk=t;
        for(j=0;j<kk;j++)
            q[t++]=p[i]*q[j]*-1;
    }
    for(i=1;i<t;i++)
        sum+=n/q[i];
    return sum;
}
int main()
{
    int t,n,m,i,j;
    scanf("%d",&t);
    while(t--)
    {
        scanf("%d%d",&n,&m);
        ll ans=n;
        for(i=2;i<=m;i++)
        {
            getp(i);
            ans+=n-solve(n);
        }
        printf("%lld\n",ans);
    }
    return 0;
}

20:30:31 你能往前走 我也厌倦了再蹉跎,紧抱住的绿洲 是残破的海市蜃楼——沈以诚《绿洲》

卡特兰数

前几项为 : 1, 2, 5, 14, 42, 132, 429, 1430, 4862, 16796, 58786, 208012, 742900, 2674440, 9694845, 35357670, 129644790, 477638700, 1767263190, 6564120420, 24466267020, 91482563640, 343059613650, 1289904147324, 4861946401452, ...定义:令h(0)=1,h(1)=1,Catalan数满足递归式:h(n) = h(0)*h(n-1) + h(1)*h(n-2) + ... + h(n-1)*h(0)  (n>=2)该递推关系的解为:h(n) = C(2n,n)/(n+1),n=0,1,2,3,... (其中C(2n,n)表示2n个物品中取n个的组合数)常用递推式:a[i,j]=a[i-1,j]+a[i,j-1]
组合公式:h(n)=C(2n,n)/(n+1) (n=0,1,2,...)组合公式:h(n)=c(2n,n)-c(2n,n-1)(n=0,1,2,...)

二叉树计数           出栈序列                  加括号               凸多边形划分

这三道题均使用了卡特兰数,都还比较简单,只是需要把卡特兰数的递推公式求出来就行

Binary Tree Number

将分子和分母因式分解,上下约分后再使用高精度算法乘得最终的解。

这道题有点难想。

#include<bits/stdc++.h>
using namespace std;
int n,l=1;
int a[1000010];
void s(int x)
{
    for(int i=1;i<=l;i++) 
    a[i]*=x;
    for(int i=1;i<=l;i++)
    {
        a[i+1]+=a[i]/10;
        a[i]=a[i]%10;
    }
    while(a[l+1])
    {
        a[l+2]=a[l+1]/10;
        a[l+1]%=10;
        l++;
    }
    return;
}
void ss(int x)
{
    for(int i=l;i>=1;i--)
    {
        a[i-1]+=a[i]%x*10;
        a[i]/=x;
    }
    for(int i=l;i>=1;i--)
    {
        if(a[i]!=0)
        {
            l=i;
            return;
        }
    }
}
int main()
{
    cin>>n;
    a[l]=1;
    for(int i=1;i<=n;i++)
    {
        s(i*4-2);
        ss(i+1);
    }
    for(int i=l;i>=1;i--) 
    printf("%d",a[i]);
    return 0;
}

20:57:01 原谅我不可自拔,可能不经意看你一眼,百米冲刺都会停下。——沈以诚《形容》

斯特林数

第一类斯特林数
1.定理
第一类斯特林数 S1(n,m) 表示的是将 n 个不同元素构成 m 个圆排列的数目。

2.递推式
设人被标上1,2,.....p,则将这 p 个人排成 m 个圆有两种情况:在一个圆圈里只有标号为 p 的人自己,排法有 S1(n-1,m-1) 个。p 至少和另一个人在一个圆圈里。

这些排法通过把 1,2....n-1 排成 m 个圆再把 n 放在 1,2....n-1 任何一人左边得到,因此第二种类型排法共有 (n-1)*S1(n-1,m) 种。

我们所做的就是把 {1,2,...,p} 划分到 k 个非空且不可区分的盒子,然后将每个盒子中的元素排成一个循环排列。

综上,可得出第一类Stirling数定理:

S1(n,m)=(n-1)*S1(n-1,m)+S1(n-1,m-1) (n>1,m>1)

边界条件:

  1. S1(n,m)=1 (n>=0):有 n 个人和 n 个圆,每个圆只有一个人

  2. S1(n,0)=0 (n>=1) :如果至少有 1 个人,那么任何的安排都至少包含一个圆

const int mod=1e9+7;//取模
LL s[N][N];//存放要求的第一类Stirling数
void init(){
    memset(s,0,sizeof(s));
    s[1][1]=1;
    for(int i=2;i<=N-1;i++){
        for(int j=1;j<=i;j++){
            s[i][j]=s[i-1][j-1]+(i-1)*s[i-1][j];
            if(s[i][j]>=mod)
                s[i][j]%=mod;
        }
    }

 

第二类斯特林数

1.定理
第二类斯特林数 S2(n,m) 表示的是把 n 个不同元素划分到 m 个集合的方案数。

2.递推式
元素在哪些集合并不重要,唯一重要的是各集合里装的是什么,而不管哪个集合装了什么。

考虑将前 n 个正整数,{1,2,...,n} 的集合作为要被划分的集合,把 {1,2,...,n} 分到 m 个非空且不可区分的集合的划分有两种情况:

那些使得 n 自己单独在一个集合的划分,存在有 S2(n-1,m-1) 种划分个数
那些使得 n 不单独自己在一个盒子的划分,存在有 m*S2(n-1,m) 种划分个数
考虑第二种情况,n 不单独自己在一个盒子,也就是 n 和其他元素在一个集合里面,也就是说在没有放 n 之前,有 n-1 个元素已经分到了m 个非空且不可区分的盒子里面(划分个数为 S2(n-1,m)),那么现在问题是把 n 放在哪个盒子里面,此时有 m 种选择,所以存在有 m*S2(n-1,m)

综上,可得出第二类斯特林数定理:
S2(n,m)=m*S2(n-1,m)+S2(n-1,m-1) (1<=m<=n-1)

边界条件:\left\{\begin{matrix}S2(n,m)=1 ,n>=0 \\S2(n,0)=0,n>=1 \end{matrix}\right. 

const int mod=1e9+7;//取模
LL s[N][N];//存放要求的Stirling数
void init(){
    memset(s,0,sizeof(s));
    s[1][1]=1;
    for(int i=2;i<=N-1;i++){
        for(int j=1;j<=i;j++){
            s[i][j]=s[i-1][j-1]+j*s[i-1][j];
            if(s[i][j]>=mod)
                s[i][j]%=mod;
        }
    }
}

来源于百度百科

 高精度模板

const int BITLEN = 100000000, BIGSIZE = 1500;
struct Big{
    long long val[BIGSIZE], len;
    void operator = (long long x){
        memset(val, 0, sizeof(val));
        val[len = 1] = x;
        while(val[len] >= BITLEN)
            val[len+1] = val[len] / BITLEN, val[len] %= BITLEN, len++;
    }
    void read(){
        char inp[BIGSIZE * 8];
        int top = -1;
        memset(inp, '0', sizeof(inp));
        do inp[++top] = getchar();
        while(isdigit(inp[top]));
        inp[top] = '0';
        reverse(inp, inp+top);
        for(int i=1; (i-1)*8<top; i++, len++) for(int j=i*8; j>(i-1)*8; j--)
            val[i] = val[i] * 10 + (inp[j-1] ^ 48);
        len--;
    }
    Big(int x = 0){this->operator = (x);}
    Big operator + (Big x){
        Big ans;
        ans.len = max(len, x.len) + 1;
        for(int i=1; i<=ans.len; i++){
            ans.val[i] += val[i] + x.val[i];
            ans.val[i + 1] += ans.val[i] / BITLEN;
            ans.val[i] %= BITLEN;
        }
        while(ans.len > 1 && ans.val[ans.len] == 0)
            ans.len--;
        return ans;
    }
    Big operator - (Big x){
        Big ans;
        ans.len = max(len, x.len) + 1;
        for(int i=1; i<=ans.len; i++){
            ans.val[i] += val[i] - x.val[i] + BITLEN, ans.val[i + 1]--;
            ans.val[i + 1] += ans.val[i] / BITLEN;
            ans.val[i] %= BITLEN;
        }
        while(ans.len > 1 && ans.val[ans.len] == 0)
            ans.len--;
        return ans;
    }
    Big operator * (Big x){
        Big ans;
        ans.len = len + x.len;
        for(int i=1; i<=len; i++)
            for(int j=1; j<=x.len; j++)
                ans.val[i + j - 1] += val[i] * x.val[j];
        for(int i=1; i<=ans.len; i++)
            ans.val[i + 1] += ans.val[i] / BITLEN, ans.val[i] %= BITLEN;
        while(ans.len > 1 && ans.val[ans.len] == 0)
            ans.len--;
        return ans;
    }
    bool operator < (Big &temp){
        if(len != temp.len) return len < temp.len;
        for(int i=len-1; i>=1; i--) if(val[i] != temp.val[i])
            return val[i] < temp.val[i];
        return false;
    }
    void print(){
        for(int i=len; i>=1; i--){
            if(i != len) for(int j=BITLEN/10; j>1; j/=10) if(val[i] < j)
                putchar('0');
            printf("%d", val[i]);
        }
    }
};
View Code

 

posted @ 2020-02-01 22:35  wybxz  阅读(666)  评论(4编辑  收藏  举报