[学习笔记] 高维前缀和
前言
怎么这么基础的东西我到快退役的时候才开始学,虽然我现在学东西还是挺快的。
下面是参考资料,当然本篇博客加入了自己的理解,话说我搞得这么正式干嘛:
- 前缀和 & 差分 - OI Wiki (oi-wiki.org)
- 题解 P5495 【模板】Dirichlet 前缀和- AThousandMoon 的博客 - 洛谷博客
- 高维前缀和总结(sosdp) - heyuhhh - 博客园
原理
设高维空间 \(U\) 一共有 \(D\) 维,需要对 \(f\) 求出高维前缀和 \(sum\),设 \(dp[i][s]\) 表示同 \(s\) 后 \(D-i\) 维相同的所有点对于 \(s\) 点的贡献。首先我们可以知道 \(dp[0][s]=f[s],dp[D][s]=sum[s]\)
有转移:\(dp[i][s]=dp[i-1][s]+dp[i][s']\),其中 \(s'\) 表示第 \(i\) 维恰好比 \(s\) 少 \(1\) 的点。为什么会存在这么简洁的递推关系呢?从贡献法的角度思考,对于点 \((p_1,p_2...p_D)\) 只会在最后一个 \(p_k<q_k\) 贡献给点 \((q_1,q_2...q_D)\),这是因为如果不是最后一个,那么 \(s'\) 是不会包含点 \(p\) 的,如果是最后一个 \(s'\) 会包含点 \(p\),这样每个点都恰好贡献了一次,就不需要复杂的容斥原理了。
时间复杂度 \(O(D\cdot |U|)\),大部分情况下非常优秀,且代码实现非常简洁。
Dirichlet 前缀和
这是高维前缀和一个经典应用,我们把数 \(x\) 做唯一分解得到 \(x=p_1^{x_1}\cdot p_2^{x_2}...p_{k}^{x_k}\),那么 \(x\) 可以贡献给 \(y\) 的充要条件是 \(\forall i,x_i\leq y_i\),这启示我们可以把质数的次幂当成坐标来做高维前缀和。
那么 \(s'\) 的求法就是除以当前这一维的质数,根据质数分布及调和级数的理论,时间复杂度 \(O(n\log \log n)\)
#include <cstdio>
#define uint unsigned int
const int M = 20000005;
int read()
{
int x=0,f=1;char c;
while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
return x*f;
}
int n,cnt,p[M],vis[M];uint seed,a[M],ans;
uint getnext()
{
seed^=seed<<13;
seed^=seed>>17;
seed^=seed<<5;
return seed;
}
void init(int n)
{
for(int i=2;i<=n;i++)
{
if(!vis[i]) p[++cnt]=i;
for(int j=1;j<=cnt && i*p[j]<=n;j++)
{
vis[i*p[j]]=1;
if(i%p[j]==0) break;
}
}
}
signed main()
{
n=read();scanf("%u",&seed);init(n);
for(int i=1;i<=n;i++)
a[i]=getnext();
for(int i=1;i<=cnt;i++)
for(int j=1;j*p[i]<=n;j++)
a[j*p[i]]+=a[j];
for(int i=1;i<=n;i++)
ans^=a[i];
printf("%u\n",ans);
}
Present for Vitalik the Philatelist
题目描述
解法
设 \(s_i\) 表示 \(\gcd=i\) 的集合个数,设 \(g_i\) 表示和 \(i\) 的互质的 \(a_x\) 个数,那么答案可以写成:
设 \(c_i\) 表示是 \(i\) 倍数的 \(a_x\) 个数,可以做高维后缀和得到(只需要改一下求和的方向即可)。
令 \(g'_i=\mu(i)\cdot c_i\),那么它做高维前缀和可以得到 \(g_i\)。
那么现在的问题是计算 \(s_i\),设 \(s_i'\) 表示 \(\gcd\) 为 \(i\) 倍数的集合个数,显然 \(s_i'=2^{c_i}-1\),并且有:
那么对 \(s_i'\) 做逆高维前缀和就可以得到 \(s_i\) 了,把高维前缀和的过程反过来即可。
时间复杂度 \(O(m\log \log m)\),其中 \(m\) 表示值域大小。
#include <cstdio>
#include <iostream>
using namespace std;
const int M = 10000005;
const int MOD = 1e9+7;
int read()
{
int x=0,f=1;char c;
while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
return x*f;
}
int n,m,cnt,ans,p[M],mu[M];bool vis[M];
int c[M],g[M],s[M],pw[M];
void init()
{
mu[1]=1;
for(int i=2;i<=m;i++)
{
if(!vis[i]) p[++cnt]=i,mu[i]=-1;
for(int j=1;j<=cnt && i*p[j]<=m;j++)
{
vis[i*p[j]]=1;
if(i%p[j]==0) break;
mu[i*p[j]]=-mu[i];
}
}
}
signed main()
{
n=read();pw[0]=1;
for(int i=1;i<=n;i++)
{
int x=read();
m=max(m,x);c[x]++;
pw[i]=pw[i-1]*2%MOD;
}
init();
//c
for(int i=1;i<=cnt;i++)
for(int j=m/p[i];j;j--)
c[j]+=c[j*p[i]];
//g
for(int i=1;i<=m;i++) g[i]=c[i]*mu[i];
for(int i=1;i<=cnt;i++)
for(int j=1;j*p[i]<=m;j++)
g[j*p[i]]+=g[j];
//s
for(int i=1;i<=m;i++) s[i]=pw[c[i]]-1;
for(int i=cnt;i>=1;i--)
for(int j=1;j*p[i]<=m;j++)
s[j]=(s[j]-s[j*p[i]]+MOD)%MOD;
//ans
for(int i=2;i<=m;i++)
ans=(ans+1ll*s[i]*g[i])%MOD;
printf("%d\n",ans);
}
Bits And Pieces
题目描述
解法
考虑枚举 \(a_i\),然后对于 \(a_i\) 的超集从大到小地考虑。
那么现在的问题是,是否能通过位置 \(>i\) 的 \(a_x\) 组成某个数字 \(s\),那么我们可以考虑 \(s\) 的超集,找出其中位置最大的两个数。可以考虑高维后缀和,只是这里加法的定义改成了 pair
的取 \(\max\)
#include <cstdio>
#include <iostream>
using namespace std;
const int M = 2100005;
int read()
{
int x=0,f=1;char c;
while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
return x*f;
}
int n,m,ans,a[M],mx[M],cx[M];
void add(int x,int y)
{
if(mx[x]<y) cx[x]=mx[x],mx[x]=y;
else if(cx[x]<y) cx[x]=y;
}
signed main()
{
n=read();m=(1<<21)-1;
for(int i=1;i<=n;i++) a[i]=read();
for(int i=0;i<=m;i++) mx[i]=cx[i]=-1;
for(int i=1;i<=n;i++) add(a[i],i);
for(int i=0;i<21;i++)
for(int s=m;s>=0;s--) if(s>>i&1)
{
add(s^(1<<i),mx[s]);
add(s^(1<<i),cx[s]);
}
for(int i=1;i<n-1;i++)
{
int res=0,lim=m^a[i];
for(int j=20;j>=0;j--) if(lim>>j&1)
if(cx[res^(1<<j)]>i) res^=(1<<j);
ans=max(ans,res|a[i]);
}
printf("%d\n",ans);
}