非空子序列乘积之和

问题描述:

给定一个包含 N 个正整数的序列 A=[a1​,a2​,…,aN​]。

目标是计算所有非空子序列的乘积之和,结果对一个给定的模数 M(1e9+7) 取模。

例如:序列A=[1,2,3]

A的子序列:{},{1},{2},{3},{1,2},{1,3},{2,3},{1,2,3}

A的子序列乘积:0,1,2,3,2,3,6,6

最终结果:1+2+3+2+3+6+6=23;

数学原理:

这个问题的结果可以用这个式子直接算出来:

\left(\prod_{i=1}^{n}(1+a_i)\right){-1} \pmod{10^9+7}

理由如下:

这个式子展开就是这样(1+a1​)(1+a2​)…(1+an​)。一个有2^n项,刚好是n个数子序列的个数。

那么就有一个合理的猜测:是否它的每一项就是一个子序列对应的乘积呢?

  • 如果你每次都选择“1”: 从 (1+a1​) 里选 1,从 (1+a2​) 里选 1,...,从 (1+an​) 里选 1。 那么乘积就是 1×1×…×1=1。 这个 1 代表什么呢?它代表了你一个数字都没选的情况,也就是空子序列的价值。

  • 如果你选择一个 ai​,其他都选择“1”: 比如,你从 (1+a1​) 里选 a1​,然后从 (1+a2​) 到 (1+an​) 里都选 1。 那么乘积就是 a1​×1×…×1=a1​。 这对应了只包含 a1​ 这一个数字的子序列的价值。 同理,你也可以得到 a2​,a3​,…,an​ 这些项,它们分别对应只包含一个数字的子序列的价值。

  • 如果你选择两个 ai​ 和 aj​,其他都选择“1”: 比如,你从 (1+a1​) 里选 a1​,从 (1+a2​) 里选 a2​,然后从 (1+a3​) 到 (1+an​) 里都选 1。 那么乘积就是 a1​×a2​×1×…×1=a1​×a2​。 这对应了包含 a1​ 和 a2​ 这两个数字的子序列的价值。 同理,你会得到所有像 ai​×aj​ 这样的项,它们对应了所有包含两个数字的子序列的价值。

  • 以此类推: 无论你选择多少个 ai​(比如 ax​,ay​,az​),剩下的都选择 1,你得到的乘积就是 ax​×ay​×az​。这精确对应了由 ax​,ay​,az​ 组成的子序列的价值。

所以我们的猜测是正确的:(1+a1​)(1+a2​)…(1+an​)每一项就是一个子序列对应的乘积。

那么最终的结果就是(1+ai)的所有的乘积减去空序列对应的1。

动态规划的角度理解:

这题可以用纯数学公式的角度来做,也可以从动态规划的角度来理解:

还是考虑序列A=[1,2,3]

A的子序列:{},{1},{2},{3},{1,2},{1,3},{2,3},{1,2,3}

此时如果我往序列A中加入一个新元素4,

A的子序列:{},{1},{2},{3},{1,2},{1,3},{2,3},{1,2,3},{4},{1,4},{2,4},{3,4},{1,2,4},{1,3,4},{2,3,4},{1,2,3,4}

可以观察到:当一个新元素加入到序列中时,新的子序列数量会翻倍。其中一半是原来序列的子序列(不包含新元素),另一半是原来序列的每个子序列分别添加这个新元素后形成的新子序列。

既然子序列的变化有规律,那么乘积的变化也有规律:

假设原来的乘积为result,那么新的乘积为result*(1+ai)

dp递推公式:result=result*(1+ai)

可以观察到这里的(1+ai)和前面的数学证明有异曲同工之妙。

dp的代码:

    for (int i = 0; i < n; ++i) {
        long long a_i;
        cin >> a_i; 
        result = (result * ((1 + a_i) % MOD)) % MOD;
    }
    cout<<result-1;//减去空序列的1;

总结:

我感觉本质上是因为子序列的变化有这样迭代的性质,所以可以动态规划;同理因为子序列本身变化有规律性,所以可以推导出相应的数学公式直接来做这道题。

所以本质上题目考察的是子序列的性质。


经典子集和问题

问题描述:

给定一个包含 N 个正整数的序列 A=[a_1,a_2,ldots,a_N]。

目标是计算所有子集中,每个子集的元素之和的总和。

例如: 序列 A=[1,2,3]

它的所有子集(包括空集)及其和分别是:

  • 空集 {}:和为 0

  • {1}:和为 1

  • {2}:和为 2

  • {3}:和为 3

  • {1, 2}:和为 1+2=3

  • {1, 3}:和为 1+3=4

  • {2, 3}:和为 2+3=5

  • {1, 2, 3}:和为 1+2+3=6

最终所有子集的和的总和是:0+1+2+3+3+4+5+6=24。

数学原理:

这个问题的结果可以用一个简洁的式子直接算出来:

\text{sum} = \left(\sum_{i=1}^{N}a_i\right) \times 2^{N-1}

理由如下:

解决这个问题的关键在于:考虑每个元素 a_i 对最终总和的“贡献”。我们不需要一个一个地列出所有子集再求和,那样效率太低了。

一个子集由原序列中的一些元素组成。对于原序列中的任何一个元素 a_i,在构造所有可能的子集时,它只有两种选择:被选中并包含在子集中,或者不被选中而不包含在子集中。

  • 对于元素 a_i 来说,它在多少个子集中会出现呢?

    • 除了 a_i 自身,原序列中还有 N−1 个其他元素。

    • 对于这 N−1 个其他元素,每个元素也都有“选中”或“不选中”两种独立的可能。

    • 因此,这 N−1 个元素可以组成 2N−1 种不同的组合(也就是 2N−1 个子集)。

    • 无论是哪一种组合,我们都可以选择将 a_i 添加到这个组合中,形成一个新的子集。

    • 这意味着,元素 a_i 将会出现在所有 2N−1 个子集中。

  • a_i 的总贡献:

    • 既然 a_i 会在 2N−1 个子集中出现,那么它就会在这 2N−1 个子集的和中各贡献一次自己的值 a_i。

    • 所以,元素 a_i 对所有子集总和的贡献是 a_i \times 2^{N-1}。

  • 最终的总和:

    • 将所有元素的贡献加起来,就是所有子集的总和。

    • \text{sum} = \sum_{i=1}^{N}(a_i \times 2^{N-1})

      这个公式可以进一步简化为: \text{sum} = \left(\sum_{i=1}^{N}a_i\right) \times 2^{N-1}

这说明了,最终的结果是所有元素之和,再乘以2^{N-1}。

动态规划的角度理解:

这题也可以从动态规划的角度来理解,其思路与“非空子序列乘积之和”的动态规划递推类似,但操作略有不同。

还是考虑序列A=[1,2,3]

A的子序列:{},{1},{2},{3},{1,2},{1,3},{2,3},{1,2,3}

此时如果我往序列A中加入一个新元素4,

A的子序列:{},{1},{2},{3},{1,2},{1,3},{2,3},{1,2,3},{4},{1,4},{2,4},{3,4},{1,2,4},{1,3,4},{2,3,4},{1,2,3,4}

可以观察到:当一个新元素加入到序列中时,新的子序列数量会翻倍。其中一半是原来序列的子序列(不包含新元素),另一半是原来序列的每个子序列分别添加这个新元素后形成的新子序列。

既然子序列的变化有规律,那么子序列和的变化也有规律:

假设原序列(例如 [1, 2, 3])的所有子集之和的总和为 original_sum (即 24)。

当新元素 a_k (例如 4) 加入时,新的总和分为两部分:

  1. 不包含 a_k 的子集之和: 这部分子集的和就是 original_sum。

  2. 包含 a_k 的子集之和:

    • 这部分是由原来的每个子集都加上 a_k 得到的。

    • 原来有 2k−1 个子集(例如对于 [1,2,3] 来说,有 2^3=8 个子集)。

    • 每个子集的和都增加了 a_k。

    • 所以,这部分的总和就等于:original_sum + (a_k 乘以 2^{k-1}(即原来的子集数量))。

new_sum = (不包含 a_k 的子集之和) + (包含 a_k 的子集之和)

new_sum = original_sum + (original_sum + a_k × 2^{k-1})

new_sum = 2 * original_sum + a_k × 2^{k-1}

这就是动态规划中的递推关系:dp[k] = 2 * dp[k-1] + a_k * 2^(k-1)。

dp[0]=0,空序列为0;

dp的代码:

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
int MOD=1e9+7;
ll fastpow(ll a,ll n,ll m){
    ll ans=1;
    a=a%m;
    while(n){
        if(n&1==1) ans=(ans*a)%MOD;
        a=(a*a)%MOD;
        n=n>>1;

    }
    return ans;
}
int main(){
    int n=0;
    cin>>n;
    vector<int> a(n+1,0);
    vector<ll> dp(n+1,0);
    for(int i=1;i<=n;i++){
        cin>>a[i];
    }
    
    for(int i=1;i<=n;i++){
        dp[i]=(2*dp[i-1])%MOD+a[i]*pow(2,i-1);
    }
    cout<<dp[n];
    return 0;
}

总结:

这其实也是由子序列特性延申出来的。


所有非空子集的异或和

问题描述:

给定一个包含 N 个数字的集合 A={a1​,a2​,…,aN​},计算它的所有非空子集中,每个子集的元素异或和的总异或和。

示例:序列 A={1,2,3}

首先,我们列出所有非空子集,并计算它们的异或和

  • 对于包含一个元素的子集:

子集 {1},异或和为 1

子集 {2},异或和为 2 

子集 {3},异或和为 3 

  • 对于包含两个元素的子集:

    • 子集 {1, 2}:1⊕2=012​⊕102​=112​=3

    • 子集 {1, 3}:1⊕3=012​⊕112​=102​=2

    • 子集 {2, 3}:2⊕3=102​⊕112​=012​=1

  • 对于包含三个元素的子集:

    • 子集 {1, 2, 3}:1⊕2⊕3=(1⊕2)⊕3=3⊕3=0

总结:

这个也是同样的利用子序列的特性可以得出规律:

在序列A=[a1.....an]中,ai为非负整数的情况下,

n=1时,result=a1;

n>1时,result=0;

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐