组合数

组合数

基本做法

先看看不做预处理计算一个组合数:

int C(int r, int n)
{
    int ans = 1;
    for (int i = 1; i <= r; i++)
    {
		ans *= n - i + 1;
        ans /= i;
    }
    return ans;
}

预处理组合数

针对大多数仅仅是利用组合数求解问题的题目运用递推法打表,不仅方便,而且可以稳稳地控制复杂度,对于需要多次引用组合数的题目效果极佳:

基于组合数公理性质:

\[C^m_n=C^{n-m}_n \]

推得:

\[C^m_n=C^{m-1}_{n-1}+C^m_{n-1} \]

由这个递推公式就可以熟练的写出组合数代码,但要注意初始化:

\[C^0_0=0 \]

\[C^i_0=C^1_0=C^1_1=1 \]

同时,把表打出来后,我们会发现这就是杨辉三角,这个三角可以解决很多问题,记住打印三角的方法也可以打出组合数。

inline void build()
{
      c[0][0]=1;
      c[1][0]=c[1][1]=1;//如上初始化,绝对绝对不能忘记或错,结合常识。
      for(int i=2;i<=2000;i++)
      {
            c[i][0]=1;
            for(int j=1;j<=2000;j++)
          		c[i][j]=c[i-1][j-1]+c[i-1][j];//递推公式。
      }
}

例题

P2822 [NOIP2016 提高组] 组合数问题

这题还需要再用前缀和来优化查询速度
前缀和可以有效减少查询统计时的复杂度,每一次查询\(O(n)\)降到\(O(1)\)
记住:上加左 减左上 加自己
\(ans[i][j]=ans[i][j-1]+ans[i-1][j]-ans[i-1][j-1]\)

inline void build()
{
  c[0][0]=1;
  c[1][0]=c[1][1]=1;
  for(int i=2;i<=2000;i++)
  {
    c[i][0]=1;
    for(int j=1;j<=i;j++)
    {
      c[i][j]=(c[i-1][j-1]+c[i-1][j])%k;
      ans[i][j]=ans[i-1][j]+ans[i][j-1]-ans[i-1][j-1];//前缀和。
      if(!c[i][j])ans[i][j]++;//如果满足结论,计数加一。
    }
    ans[i][i+1]=ans[i][i];//继承。
  }
}
inline void solve()
{
  t=read(),k=read();
  build();
  while(t--)
  {
    n=read(),m=read();
    if(m>n)printf("%lld\n",ans[n][n]);//如果m>n,ans只会达到n,只需输出ans[n,n]就可以了。
    else printf("%lld\n",ans[n][m]);
  }
}
  1. 需掌握组合数的基本两种求解方法(通项公式,递推公式),根据数据范围选定方法。
  2. 结合数据范围找到优化方法,无论是取模还是自己的玄学优化都尝试一下.(建议取模输出的题,绝对不要乱模!既费时又易错)
  3. 掌握前缀和,利用前缀和对降维的作用。

P1246 编码

cgx 举例,设 \(ans\) 为比 cgx 小的单词个数,初值为 \(0\)

\(\large1.\) 首先,cgx 肯定比只有一个字母的单词大,\(ans+26,\quad ans=26\)

\(\large2.\) 其次,cgx 肯定比只有两个字母的单词大。

只有两个字母的单词个数该怎么算呢?就是在 \(26\) 个字母中选 \(2\) 个。

\(ans+C_{26}^{2},C_{26}^{2}=\frac{26*25}{2*1}=325,\quad ans=26+325=351\)

\(\large3.\) 只有三个字母

\(3.1.\) 第一位:它比以字母 a-b 为第一位的大( cgx 第一位为 c,所以要比 c 小)

  • a 为第一位,且有三位的单词个数:\(C_{25}^{2}=\frac{25*24}{2*1}=300\)

    a 大的剩下 \(25\) 个字母中选 \(2\) 个字母:
    \(ans+300,\quad ans=351+300=651\)

  • b 为第一位,且有三位的个数:\(C_{24}^{2}=\frac{24*23}{2*1}=276\)

    \(b\) 大的剩下 \(24\) 个字母中选 \(2\) 个字母:\(ans+276,\quad ans=651+276=927\)

\(3.2\) 第二位(即第一位已经确定为 \(c\)):它比以字母 d-f 为第二位的单词大 (第一位为 c ,所以要比 c 大,第二位为 g,所以要比 g 小)

  • 第二位为 d 的个数:\(C_{22}^{1}=\frac{22}{1}=22\)

    从比 d 大的剩下 \(22\) 个字母中选 \(1\) 个字母:\(ans+22,\quad ans=927+22=949\)

  • 第二位为 e 的个数:\(C^{1}_{21}=21,ans+21,\quad ans=949+21=970\)

  • 第二位为 f 的个数:\(C^{1}_{20}=20,ans+20,\quad ans=970+20=990\)

\(3.3\) 第三位(一,二位已经确定):它比以字母 h-w 为第三位的大,共有 \(16\) 个。

\[\sum_{n=8}^{23}C^{0}_{n}=\sum_{n=8}^{23}1=16*1=16 \]

\(ans+16,\quad ans=990+16=1006\)

因为比 cgx 小的单词共有 \(1006\) 个,所以 cgx 是第 \(1007\) 个。


先判断该单词是否存在,再一位一位地去考虑共有多少比给出单词小的单词,如果用的是字符串,则还需要考虑一下边界问题。

#include<bits/stdc++.h>
using namespace std;
string s;
int ans,n;
int c(int m,int n)//组合数计算 
{
	if(m==0)return 1;
	int mut=1;
	for(int i=n;i>n-m;i--)mut*=i;
	for(int i=m;i>1;i--)mut/=i;
	return mut;
}
int main()
{
	cin>>s,n=s.size();
	for(int i=1;i<n;i++)
		if(s[i]<=s[i-1])cout<<0,exit(0);//判断是否存在
	for(int i=1;i<n;i++)ans+=c(i,26);//计算出位数比它小的单词数
	for(int i=0;i<n;i++)//枚举每一位
		for(char j=(i==0?'a':s[i-1]+1);j<s[i];j++)//注意考虑边界
			ans+=c(n-i-1,'z'-j);//因为字符串下标从0开始,所以是n-i-1而不是n-i
	cout<<++ans;//别忘了最后把自己加上
	return 0;
}
posted @ 2023-12-02 16:16  加固文明幻景  阅读(12)  评论(0编辑  收藏  举报