「NOI Online #3 提高组」优秀子序列

Description

给定一个长度为 n 的非负整数序列 A={a1,a2,...,an},对于 A 的一个子序列 B={ab1,ab2,...,abm}(1≤m≤n,1≤b1<b2<...<bm≤n,下同),称 B 是 A 的优秀子序列当且仅当,其任意两个不同元素的按位与结果均为 0,即:∀1≤i<j≤m,满足:abi and abj=0,其中 and 是按位与运算。

对于子序列 B={ab1,ab2,...,abm},我们定义其价值为 φ(1+abi),i∈[1,m],其中 φ(x) 表示小等于 x 的正整数中与 x 互质的数的个数。

现在请你求出 A 的所有优秀子序列的价值之和,答案对 109+7 取模。

Input

第一行一个正整数 n 表示序列长度。

第二行 n 个用空格分隔的非负整数,表示 a1,a2,...,an

Output

仅一行一个整数,表示答案对 109+7 取模的结果。

Solution

分析

注意到子序列中任意两个不同的数按位与等于 0,每个二进制位最多在子序列中出现一次,说明相加不可能产生进位。

则一个子序列的和不会超过 218。记 d=log2 max{Ai}。

先用线性筛预处理 218 以内的欧拉函数。

积性函数:对于所有互质的整数 x 和 y,即 gcd(x,y)=1,有性质 f(xy)=f(x)f(y) 的数论函数。

欧拉函数是积性函数,即对于 gcd(x,y)=1,有性质 φ(xy)=φ(x)φ(y)。

对于一个素数 p,φ(p)=p-1,φ(pk)=pk-1(p-1)。

对于 10% 的数据,保证 n≤20

子序列的个数只有 2n 个,直接爆搜每个数选/不选。

(不知道筛到 218会不会全部TLE……没尝试过2333)

//10%
#include<bits/stdc++.h>
#define int long long 
using namespace std;
const int N=1e6+5,mod=1e9+7;
int n,a[N],b[N],ans,p[N],phi[N],cnt;
bool vis[N];
void dfs(int x){
    if(x==n+1){
        int res=0,k=0;
        bool flag=1;
        for(int i=1;i<=n;i++)
            if(vis[i]) b[++k]=a[i],res+=a[i];
        for(int i=1;i<=k;i++){ 
            for(int j=i+1;j<=k;j++)
                if((b[i]&b[j])!=0){flag=0;break;}
            if(!flag) break; 
        } 
        if(flag) ans=(ans+phi[res+1]%mod)%mod;
        return ;
    }
    vis[x]=1,dfs(x+1),vis[x]=0,dfs(x+1);
}
signed main(){
    //freopen("sequence.in","r",stdin);
    //freopen("sequence.out","w",stdout);
    phi[1]=1;
    for(int i=2;i<=N;i++){
        if(!p[i]) p[++cnt]=i,phi[i]=i-1;
        for(int j=1;j<=cnt&&i*p[j]<=N;j++){
            p[i*p[j]]=1;
            if(i%p[j]==0){
                phi[i*p[j]]=phi[i]*p[j];
                break;
            }
            phi[i*p[j]]=phi[i]*(p[j]-1);
        }
    }
    scanf("%lld",&n);
    for(int i=1;i<=n;i++)
        scanf("%lld",&a[i]);
    dfs(1),printf("%lld\n",ans);
    return 0;
}

对于 10% 的数据,保证 ai≤1

记 cnts 表示数列中 Ai=s 的个数。

观察一下满足条件子序列的性质,可以得知至多选择一个 1(如果选 2 个按位与就不等于 0 了),而序列中的 0 可以任意选择。

可以先不考虑序列中的 0,最后的答案乘以 2cnt0

只需考虑不选或选择一个 1。

//10% 
#include<bits/stdc++.h>
#define int long long 
using namespace std;
const int N=1e6+5,mod=1e9+7;
int n,a[N],cnt;
int mul(int x,int n,int mod){
    int ans=mod!=1;
    for(x%=mod;n;n>>=1,x=x*x%mod)
        if(n&1) ans=ans*x%mod;
    return ans;
}
signed main(){
    //freopen("sequence.in","r",stdin);
    //freopen("sequence.out","w",stdout);
    scanf("%lld",&n);
    for(int i=1;i<=n;i++)
        scanf("%lld",&a[i]),cnt+=a[i];
    printf("%lld\n",(cnt+1)*mul(2,n-cnt,mod)%mod);
    return 0;
}

对于 30% 的数据,保证 ai≤1000

方法1:依然可以先不考虑序列中的 0。

注意到序列中有若干个数字 v,但是只能取至多 1 个,取的话则有 cntv  种选择。

从数值上来考虑。

设 fi,表示当前选了 i 个非 0 数,这些数的和(按位或)等于 s 的方案数。

以 i 为阶段转移,每次加入一个数 t,满足 s and t=0。

fi+1,s+t ← fi,s×cntt

由于一个长度为 i 的子序列会由于顺序原因被计算了 i! 次,则和为 s 的子序列的方案数 dps=fi,s/i!。

时间复杂度:O(4dd)。

方法2:从小到大枚举数字 t 加入,有 dps+t+=dps×cntt(s and t=0)。

直接暴力,时间复杂度:O(4d)。

对于 60% 的数据,保证 ai≤30000

考虑优化上述 30% 部分分的方法1,DP 转移时枚举的 t 可以看做是 s 的子集。

fi,s=fi-1,s-t×cntt,其中t⊂s。

用枚举子集的方法枚举 t 转移。

算法复杂度可优化至 O(3dd)。

对于 100% 的数据,保证 1≤n≤106,0≤ai≤2×105

由于上述做法要处理由于顺序原因带来的算重问题,所以要记一维DP状态 i。

按照特定顺序加入数字可以避免数据问题,比如按照最低位的大小顺序加入:

dps=dps-t×cntt

其中,t⊂s,lowbit(s)=lowbit(t)。

另外从大到小枚举 t 的方法直接改成枚举子集,也可以做到相同的复杂度。

统计答案即:ans=Σdpx×φ(1+x)×2cnt0

时间复杂度:O(3d)。

代码鸽了

猜你喜欢

转载自www.cnblogs.com/maoyiting/p/12960337.html
今日推荐