组合数+dp 运

   

                                                                        问题 B: 运

时间限制: 1 Sec  内存限制: 128 MB

题目描述

【问题背景】
zhx 和妹子们玩数数游戏。

【问题描述】

仅包含4或7的数被称为幸运数。一个序列的子序列被定义为从序列中删去若干个数, 剩下的数组成的新序列。两个子序列被定义为不同的当且仅当其中的元素在原始序列中的下标的集合不相等。对于一个长度为 N的序列,共有 2^N个不同的子序列。( 包含一个空序列)。一个子序列被称为不幸运的, 当且仅当其中不包含两个或两个以上相同的幸运数。对于一个给定序列, 求其中长度恰好为 K 的不幸运子序列的个数, 答案 mod 10^9+7 输出。

【输入格式】

第一行两个正整数 N, K, 表示原始序列的长度和题目中的 K。

接下来一行 N 个整数 ai, 表示序列中第 i 个元素的值。

【输出格式】

仅一个数,表示不幸运子序列的个数。( mod 10^9+7)

【样例输入】
3 2
1 1 1
【样例输出】
3
【样例输入】
4 2
4 7 4 7
【样例输出】
4

【样例解释】
对于样例 1,每个长度为 2 的子序列都是符合条件的。

对于样例 2,4个不幸运子序列元素下标分别为:{1, 2}, {3, 4}, {1, 4}, {2, 3}。注意下标集{1, 3}对应的子序列不是“不幸运”的, 因为它包含两个相同的幸运数4.
【数据规模与约定】
对于50%的数据, 1 ≤N ≤ 16。

对于70%的数据, 1 ≤ N ≤ 1000, ai ≤ 10000。

对于100%的数据, 1 ≤ N ≤ 100000,K ≤ N, 1 ≤ ai ≤ 109。

        考试时读错题了,幸运数是指这个数字里只包含4或7,而两者皆有也是幸运数。10^9里幸运数不太多,可以打个表看看。统计所有出现的幸运数个数,如果某个唯一,删掉直接当普通数处理,因为无论如何也构不出两个相同的来成为幸运数。

   设幸运数种数为tot,剩下数个数为cnt

                     min(tot,k)

       ans=  Σ    C (cnt,k-i)*dp[tot][i];

              i=0

    就是说,求k个数中放了i个不重复的幸运数的方案之和。

     dp数组的转移:dp[i][j]=dp[i-1][j]+dp[i-1][j-1]*c[i];

     i为推到第几种幸运数,j表示选了几个,c[i]即为这种幸运数的总个数。

     求组合数可以考虑用lucas

#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<algorithm>
#include<iostream>
#include<map>
#define ll long long
#define mod 1000000007
#define inf 1000000000
using namespace std;
ll read()
{
    ll sum=0,f=1;char x=getchar();
    while(x<'0'||x>'9'){if(x=='-')f=-1;x=getchar();}
    while(x>='0'&&x<='9'){sum=sum*10+x-'0';x=getchar();}
    return sum*f;
}
ll n,a[100005],k,tot,cnt,ans=0,f[2000][2000],c[2000],hh,b[2000];
map<int,int> mp;
int check(int x)
{
    int p=x;
    while(x!=0)
    {
        int k=x%10;
        if(k!=4&&k!=7)return 0;
        x/=10;
    }
    mp[p]=++hh;
    return 1;
}
ll cheng(ll x,ll m)
{
    ll ans=1;
    while(m)
    {
        if(m&1)ans=(ans*x)%mod;
        x=(x*x)%mod;
        m/=2;
    }
    return ans;
}
ll get(ll n,ll m)
{
    if(n<m)return 0;
    if(m>n-m)m=n-m;
    ll s1=1,s2=1;
    for(int i=0;i<m;i++)
    {
        s1=s1*(n-i)%mod;
        s2=s2*(i+1)%mod;
    }
    return (s1*cheng(s2,mod-2))%mod;
}
int lucas(int n,int m)
{
    if(m==0)return 1;
    return get(n%mod,m%mod)%mod*(lucas(n/mod,m/mod))%mod;
}
void dp()
{
    for(int i=0;i<=tot;i++)
        f[i][0]=1;
    for(int i=1;i<=tot;i++)
        for(int j=1;j<=min(k,tot);j++)
            f[i][j]=(f[i-1][j]+f[i-1][j-1]*b[i])%mod;
}
int main()
{
//  freopen("lucky.in","r",stdin);
//  freopen("lucky.out","w",stdout);
    n=read();k=read();
    for(int i=1;i<=n;i++)
    {
         a[i]=read();
         if(mp.count(a[i])||check(a[i]))
         {
            c[mp[a[i]]]++;
         }
         else
            cnt++;
    }
    for(int i=1;i<=hh;i++)
      if(c[i]>1)
        {
            b[++tot]=c[i];
        }
        else
          cnt++;
    dp();
    for(int i=0;i<=min(tot,k);i++)
    {
        ans=(ans+((lucas(cnt,k-i)%mod)*(f[tot][i]%mod))%mod)%mod;
    }
    cout<<ans;
}

     
<think>好的,我现在要解决用户的问题:如何用C++实现从长度为n的组中选取m个数的方法。用户之前提到过类似的问题,比如引用[1]中的穷举方法,还有引用[3]的动态规划,以及引用[4]的组合生成。需要结合这些信息给出一个清晰的回答。 首先,我需要确定用户的需求。用户可能是在寻找不同的方法来实现这个功能,可能需要了解不同的实现方式及其优缺点。根据引用内容,已有的方法包括位算穷举、递归回溯、动态规划和库函方法。应该逐一分析这些方法,并给出示例代码。 位算方法在引用[1]中已经提到,通过遍历所有可能的二进制位组合,检查1的个数是否为m,然后提取对应的元素。这种方法适合小规模据,因为时间复杂度是O(2^n),n大了会非常慢。需要解释清楚这个方法的原理,并提供代码示例。 递归回溯法在引用[4]中提到,Java中的递归实现,可以转化为C++。递归法更直观,但同样存在效率问题,不过对于组合生成来说,递归是常见的方法。需要展示如何用递归生成所有组合,并处理选取m个数的逻辑。 动态规划在引用[3]中用于计算方案,但用户的问题可能更关注生成具体的组合,而不是计。所以动态规划可能不太适合生成所有组合,但可以作为补充说明,如果用户需要知道方案,可以参考这个方法。不过这里可能需要调整,先专注于生成方法。 库函方法,比如使用C++的next_permutation者组合函,但需要结合判断选取m个数的逻辑。者利用STL的combination方法,比如用vector<bool>的标记来选择元素。这部分需要示例说明。 接下来要考虑用户可能的疑问,比如各种方法的适用场景,时间复杂度的比较,以及如何优化。例如,当n较大时,位算和递归可能不够高效,这时候可能需要剪枝者其他优化手段。 然后需要验证各个方法的正确性。比如位算方法中的循环是从1到2^n,每次检查1的位是否为m,然后收集对应的元素。递归方法需要正确维护当前路径和起始索引,避免重复组合。库函方法需要正确使用prev_permutation来生成组合。 此外,用户可能需要了解这些方法的应用场景,比如位算适合小据,递归适合中等据,而动态规划适合计问题。需要明确说明这些优缺点。 最后,生成相关问题,引导用户进一步思考,比如性能优化、去重处理、大据情况下的解决方案等。 现在需要组织这些思路,分步骤详细解释每种方法,提供代码示例,并确保语法正确,符合中文回答的要求。同时引用相关的参考文献,如引用[1]的位算方法,引用[4]的递归组合生成,引用[3]的动态规划思路,但注意用户的问题是关于生成组合而非计,所以动态规划可能不是主要方法,但可以提及。 需要确保代码示例正确,比如位算中的循环范围是否正确,递归中的参传递是否正确,库函的使用是否恰当。比如在递归中,使用start参来避免重复组合,确保生成的组合是按顺序选取的。 总结回答结构:先介绍不同方法,每种方法的原理、代码示例、优缺点,最后相关问题。确保引用正确,例如引用[1]中的位算方法,引用[4]的递归方法,引用[3]的动态规划思路。</think>在C++中从长度为$n$的组选取$m$个数的实现方法主要有以下几种,根据具体需求可选择不同方案: ### 一、位算穷举法 通过二进制位表示选择状态,时间复杂度为$O(2^n)$,适用于$n \leq 20$的小规模据[^1]。 ```cpp #include <vector> #include <iostream> using namespace std; int countBits(int num) { int cnt = 0; while(num) { num &= (num-1); cnt++; } return cnt; } void selectNumbers(vector<int>& arr, int m) { int n = arr.size(); for(int mask=0; mask<(1<<n); ++mask) { if(countBits(mask) != m) continue; for(int i=0; i<n; ++i) if(mask & (1<<i)) cout << arr[i] << " "; cout << endl; } } ``` ### 二、递归回溯法 通过DFS生成组合,时间复杂度为$C(n,m)$,空间复杂度$O(m)$,适合中等规模据[^4] ```cpp void dfs(vector<int>& arr, int start, vector<int>& path, int m) { if(path.size() == m) { for(int num : path) cout << num << " "; cout << endl; return; } for(int i=start; i<arr.size(); ++i) { path.push_back(arr[i]); dfs(arr, i+1, path, m); // 注意i+1避免重复 path.pop_back(); } } void combinationDFS(vector<int>& arr, int m) { vector<int> path; dfs(arr, 0, path, m); } ``` ### 三、STL库函法 利用`prev_permutation`生成组合标记,时间复杂度与递归法相当 ```cpp void combinationSTL(vector<int>& arr, int m) { vector<bool> select(arr.size(), false); fill(select.end()-m, select.end(), true); // 最后m位设为true do { for(int i=0; i<arr.size(); ++i) if(select[i]) cout << arr[i] << " "; cout << endl; } while(next_permutation(select.begin(), select.end())); } ``` ### 四、动态规划法(求方案) 当需要计算组合数而不需要具体组合时,可用DP优化,时间复杂度$O(nm)$[^3] ```cpp int countCombinations(vector<int>& arr, int m) { vector<vector<int>> dp(m+1, vector<int>(arr.size()+1, 0)); dp[0][0] = 1; for(int i=1; i<=arr.size(); ++i) for(int j=0; j<=m; ++j) dp[j][i] = dp[j][i-1] + (j>0 ? dp[j-1][i-1] : 0); return dp[m][arr.size()]; } ``` **方案对比**: 1. 位算法:实现简单但效率低 2. 递归法:直观易扩展(可添加剪枝条件) 3. STL法:代码简洁但需注意元素顺序 4. 动态规划:适合需计的情况
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值