可能用的上的小知识: 超集
如果集合A是集合B的子集,那么B就是A的超集
没了。(定义很简单,但是因为见的不多,简单介绍一下
正文
我们考虑一个关于求和的问题
,求,这里 j属于i 的定义可以很宽泛,我们可以将其定义为j|i,也可以是j的二进制表示是i的二进制表示的子集等等
显然一种暴力的方法就是枚举i的所有子集,考虑优化的话,我们可以尝试前缀和,因为显然如果有,我们可以将j的贡献都先算在z上面再传给i,而不必一个个来
但是这样的话,就涉及到了高维的前缀和处理
先来看看一维前缀和是怎么写的
for(int i=1;i<=n;++i)
{
a[i]+=a[i-1];
}
然后是二维前缀和,这里我们一般用容斥来处理
for(int i=1;i<=n;++i)
{
for(int j=1;j<=m;++j)
{
sum[i][j]+=sum[i-1][j]+sum[i][j-1]-sum[i-1][j-1];
}
}
如果到了三维的话,也是可以容斥的,写起来就有点烦
实际上除了容斥,我们还有另外一种写前缀和的方法
二维
for(int i=1;i<=n;++i)
{
for(int j=1;j<=m;++j)
{
sum[i][j]+=sum[i][j-1];
}
}
for(int i=1;i<=n;++i)
{
for(int j=1;j<=m;++j)
{
sum[i][j]+=sum[i-1][j];
}
}
这个其实很好理解
我们先按绿色方向把每一层的前缀和都给做出来,然后再沿黄色方向做一遍前缀和,按顺序从小往大走的话,显然我们会把第一维(行)的前缀和算进去。
所以不难得到三维的前缀和
for(int i=1;i<=n;++i)
{
for(int j=1;j<=m;++j)
{
for(int k=1;k<=q;++k)
{
sum[i][j][k]+=sum[i-1][j][k];
}
}
}
for(int i=1;i<=n;++i)
{
for(int j=1;j<=m;++j)
{
for(int k=1;k<=q;++k)
{
sum[i][j][k]+=sum[i][j-1][k];
}
}
}
for(int i=1;i<=n;++i)
{
for(int j=1;j<=m;++j)
{
for(int k=1;k<=q;++k)
{
sum[i][j][k]+=sum[i][j][k-1];
}
}
}
那么如果扩展到了n维会怎么样?
思路跟上面是一样的,就是一维一维的做前缀和
二进制子集前缀和
考虑一开始的问题: ,,求 ,其中 j属于i 定义为j的二进制表示是i的二进制表示的子集
for(int j=0;j<n;++j)
{
for(int i=0;i<(1<<n);++i)
{
if(i&(1<<j))
{
dp[i]+=dp[i^(1<<j)]
}
}
}
因为是正序枚举的,所以i^(1<<j)是是当前这一维度,而此时i还在上一维,所以这样就实现了高维的前缀和。这样的时间复杂度是,而如果去枚举子集来做前缀和的话,时间复杂度是n^3的。
我们来看一些具体的应用
大意:
给定一个长度为2^n的数组,对于每一个k,1<=k<=2^n-1,求出最大的ai+aj,其中iorj<=k
思路:
关键就是如何处理i or j<=k,不难发现这蕴含的意思其实就是iorj的结果是k的二进制表示下的子集
所以我们直接跑高维前缀和,维护一下每一个k对应的能够用的最大值以及次大值即可
code
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define endl '\n'
const ll N=1e5+10;
ll n;
ll mas[N],sec[N],dp[N];
void upt(ll id,ll val)
{
if(val>=mas[id])
{
sec[id]=mas[id];
mas[id]=val;
}
else if(val>=sec[id])
{
sec[id]=val;
}
}
void solve()
{
cin>>n;
for(int i=0;i<(1<<n);++i) cin>>mas[i];
for(int j=0;j<n;++j)
{
for(int i=0;i<(1<<n);++i)
{
if(i&(1<<j))
{
upt(i,mas[i^(1<<j)]);
upt(i,sec[i^(1<<j)]);
}
}
}
for(int i=1;i<(1<<n);++i)
{
dp[i]=max(dp[i-1],mas[i]+sec[i]);
cout<<dp[i]<<endl;
}
}
int main()
{
// ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
solve();
return 0;
}
再看一道
给定一个数组,问里面有多少个数字满足ai&aj=0
思路:
显然对于一个数ai,我们找到它的二进制表示的补集Q,那么Q的所有子集出现过的次数就是ai的贡献,所以我们还是可以通过高维前缀和来处理
代码不写了,题源也找不到,自己意会一下
那么上面都是二进制枚举子集的前缀和,如果是枚举超集的话,那其实叫后缀和会更加合理一些吧
二进制超集后缀和
代码也很好写,无非就是把原本为0的地方改成1
for(int j=0;j<n;++j)
{
for(int i=0;i<(1<<n);++i)
{
if((i&(1<<j))==0)
{
dp[i]+=dp[i^(1<<j)]
}
}
}
大意:
n,ai<=1e6
思路:
不考虑复杂度的话我们有一个非常套路的容斥做法。考虑性质Ai表示子集与之后第i位为1,那么我们的答案其实就是
其中||符号就表示集合的大小
显然就可以状压枚举,这样的时间复杂度是O(n*1e6),考虑优化。
注意到对于,我们记满足对应所有性质的元素的个数为k,则该集合的大小就是2^k-1,那么什么元素会满足这些性质呢?就是二进制为其超集的元素呗,其价值就是1.
所以我们只要做一遍超集后缀和即可,时间复杂度来到O(20*1e6)
code
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define endl '\n'
const ll N=1e5+10;
const ll mod=1e9+7;
ll n,cnt=0,a;
ll mas[N];
ll up=20;
ll vis[30];
ll dp[(1<<20)+10];
ll ksm(ll x,ll y)
{
ll ans=1;
while(y)
{
if(y&1) ans=ans*x%mod;
x=x*x%mod;
y>>=1;
}
return ans;
}
ll gt()
{
ll tot=0;
ll fl;
for(int i=1;i<=n;++i)
{
fl=1;
for(int j=0;j<up;++j)
{
if(!vis[j]) continue;
if((mas[i]&(1<<j))==0)
{
fl=0;
break;
}
}
if(fl) tot++;
}
return ((ksm(2,tot)-1)%mod+mod)%mod;
}
void solve()
{
cin>>n;
for(int i=1;i<=n;++i) cin>>a,dp[a]++;
for(int j=0;j<up;++j)
{
for(int i=0;i<(1<<up);++i)
{
if((i&(1<<j))==0) dp[i]+=dp[i^(1<<j)];
}
}
ll ans=0;
for(int s=0;s<(1<<up);++s)
{
cnt=0;
for(int i=0;i<up;++i) if(s&(1<<i)) cnt++;
if(cnt%2) ans=((ans-ksm(2,dp[s])+1)%mod+mod)%mod;
else ans=((ans+ksm(2,dp[s])-1)%mod+mod)%mod;
}
cout<<ans<<endl;
}
int main()
{
ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
solve();
return 0;
}
然后我们看看一开始提出的第二种定义
,,求 ,其中 j属于i 定义为j | i
显然j|i蕴含的意思是,对于每一个质因子p,j蕴含的p的幂次不大于i蕴含的p的幂次,所以这里还是一个子集的关系,只不过集合的定义由二进制表示变成了素数分解
我们换一种写法
,求
我们只要按照埃氏筛的样子做一遍更新就可以了
for(int i=1;i<=n;++i)
{
if(!vis[i])
{
for(int j=1;j*i<=n;++j) sum[i*j]+=sum[j],vis[i*j]=1;
}
}
如果要预处理的话也可以,唯一需要注意的,跟上文一样,这里我们的维度是由不同的素因子来确定的,所以我们要先枚举素因子来保证每一维的前缀和都更新全了
for(int i=1;i<=totprime;++i)
{
for(int j=1;p[i]*j<=n;++j)
{
sum[p[i]*j]+=sum[j];
}
}
那么这种东西其实有一种更加正式的名字:
狄利克雷前缀和
大意:如上
思路:如上
code
#include<bits/stdc++.h>
using namespace std;
#define ll unsigned int
#define endl '\n'
const ll N=2e7+10;
ll n,a;
ll seed;
inline ll getnext(){
seed^=seed<<13;
seed^=seed>>17;
seed^=seed<<5;
return seed;
}
ll b[N];
bool vis[N];
ll ans=0;
void solve()
{
cin>>n>>seed;
for(int i=1;i<=n;++i) b[i]=getnext();
vis[1]=1;
for(int i=1;i<=n;++i)
{
if(!vis[i])
{
for(int j=1;j*i<=n;++j) b[i*j]+=b[j],vis[i*j]=1;
}
}
for(int i=1;i<=n;++i) ans^=b[i];
cout<<ans<<endl;
}
int main()
{
ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
solve();
return 0;
}
事实上还有一种东西叫做
狄利克雷后缀和
,求
其实也不难理解,就是素因子分解下的枚举超集的后缀和,跟上面讲的二进制超集后缀和一个意思
for(int i=1;i<=totprime;++i)
{
for(int j=n/p[i];j;--j)
{
sum[j]+=sum[j*p[j]];
}
}
但是因为这里我们需要用到比自己大的值,所以内层需要倒序更新
cf 757 D2. Divan and Kostomuksha (hard version)
大意:
给定一个数组a,要求重排数组,使得数组的前缀gcd之和最大
思路:
一个显然的小贪心:如果一开始的gcd是x的话,我们一定要尽可能多的保留gcd为x,因为后面的gcd不会大于x。要做到这一点,我们需要统计有多少个数字是x的倍数
换句话说,我们需要统计cnt【i】,表示有多少个数字是i的倍数,这其实就是一个狄利克雷后缀和
这里还有一个性质:cnt【j】一定不大于cnt【i】(j|i),这一点显然
那么我们如果以i作为一开始的gcd的话,会保留cnt【i】个前缀gcd为i的长度,如果以j作为一开始的gcd的话,会保留cnt【j】个前缀gcd为j的长度,这个长度肯定是不大于上一个的,所以我们可以从i转移到j,
设dp【i】表示一开始的gcd为i的情况下数组的最大价值
dp[j]=max(dp[j],1ll*cnt[j]*(j-i)+dp[i]);
一开始的初始条件是dp[1]=1
另外这题数据范围有点大,需要预处理一下素数,然后按素数转移即可
code
#include<bits/stdc++.h>
using namespace std;
#define ll int
#define endl '\n'
const ll N=2e7;
ll n,a;
ll cn=0;
ll cnt[N+10];
long long dp[N+10];
ll p[N+10];
bool vis[N+10];
void init()
{
for(int i=2;i<=N;++i)
{
if(!vis[i])
{
p[++cn]=i;
}
for(int j=1;j<=cn&&i*p[j]<=N;++j)
{
vis[i*p[j]]=1;
if(i%p[j]==0) break;
}
}
}
void solve()
{
init();
cin>>n;
//cout<<cn<<endl;
for(int i=1;i<=n;++i)
{
cin>>a;
cnt[a]++;
}
for(int i=1;i<=cn;++i)
{
for(int j=N/p[i];j;--j)
{
cnt[j]+=cnt[j*p[i]];
}
}
dp[1]=n;
long long ans=0;
for(int i=1;i<=N;++i)
{
for(int j=1;i*p[j]<=N;++j)
{
ll sd=i*p[j];
dp[sd]=max(dp[sd],1ll*cnt[sd]*(sd-i)+dp[i]);
}
ans=max(ans,dp[i]);
}
cout<<ans<<endl;
}
int main()
{
ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
solve();
return 0;
}