高维前缀和 学习笔记

可能用的上的小知识: 超集

如果集合A是集合B的子集,那么B就是A的超集

没了。(定义很简单,但是因为见的不多,简单介绍一下


正文

我们考虑一个关于求和的问题

\large \forall i,求\large \sum_{j\subset i} a_{j},这里 j属于i 的定义可以很宽泛,我们可以将其定义为j|i,也可以是j的二进制表示是i的二进制表示的子集等等

显然一种暴力的方法就是枚举i的所有子集,考虑优化的话,我们可以尝试前缀和,因为显然如果有\large j\subset z ,z \subset 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维会怎么样?

思路跟上面是一样的,就是一维一维的做前缀和

二进制子集前缀和

考虑一开始的问题: \forall i,0\leq i\leq 2^n-1,,求\large \sum_{j\subset i} a_{j} ,其中 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还在上一维,所以这样就实现了高维的前缀和。这样的时间复杂度是O(n*2^n),而如果去枚举子集来做前缀和的话,时间复杂度是n^3的。

我们来看一些具体的应用

ARC 100 E - Or Plus Max

大意:
给定一个长度为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)]
        }
    }
}

Jzzhu and Numbers

大意:

n,ai<=1e6

思路:
不考虑复杂度的话我们有一个非常套路的容斥做法。考虑性质Ai表示子集与之后第i位为1,那么我们的答案其实就是|\Omega -A_1\bigcup A_2...\bigcup A_{20}|=\sum_{i=0}^{20}(-1)^i\sum_{1\leq j_1 < j_2...<j_i \leq 20 }|A_{j_1}\bigcup A_{j_2}...A_{j_i}| 

其中||符号就表示集合的大小

显然就可以状压枚举,这样的时间复杂度是O(n*1e6),考虑优化。

注意到对于|A_{j_1}\bigcup A_{j_2}...A_{j_i}|,我们记满足对应所有性质的元素的个数为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;
}

然后我们看看一开始提出的第二种定义

 \forall i,0\leq i\leq 2^n-1,,求\large \sum_{j\subset i} a_{j} ,其中 j属于i 定义为j | i

显然j|i蕴含的意思是,对于每一个质因子p,j蕴含的p的幂次不大于i蕴含的p的幂次,所以这里还是一个子集的关系,只不过集合的定义由二进制表示变成了素数分解

我们换一种写法

\forall i,1\leq i\leq n,求\sum_{j|i}aj

我们只要按照埃氏筛的样子做一遍更新就可以了

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];
    }
}

那么这种东西其实有一种更加正式的名字:

狄利克雷前缀和

Dirichlet 前缀和

大意:如上

思路:如上

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;
}

事实上还有一种东西叫做

狄利克雷后缀和

\forall i,1\leq i \leq n,求\sum_{i|j}a_j

其实也不难理解,就是素因子分解下的枚举超集的后缀和,跟上面讲的二进制超集后缀和一个意思

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;
}

  • 7
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
高维的NEW和delete操作可以通过嵌套的方式来实现。例如,对于一个二维数组,可以使用两个嵌套的循环来分别分配和释放内存。首先,使用一个循环来分配一维数组的内存,然后在每个一维数组中使用另一个循环来分配二维数组的内存。在释放内存时,需要按相反的顺序进行操作,先释放二维数组的内存,然后再释放一维数组的内存。 以下是一个示例代码,演示了如何使用嵌套的NEW和delete操作来处理一个二维数组: ```cpp #include <iostream> int main() { int m = 3; // 行数 int n = 4; // 列数 // 分配内存 int** p = new int*\[m\]; for (int i = 0; i < m; i++) { p\[i\] = new int\[n\]; } // 使用数组 for (int i = 0; i < m; i++) { for (int j = 0; j < n; j++) { p\[i\]\[j\] = i + j; } } // 释放内存 for (int i = 0; i < m; i++) { delete\[\] p\[i\]; } delete\[\] p; return 0; } ``` 在这个示例中,我们首先使用两个嵌套的循环来分配内存,然后使用两个嵌套的循环来使用数组,最后按相反的顺序使用两个嵌套的循环来释放内存。这样可以确保内存的正确分配和释放。 #### 引用[.reference_title] - *1* *3* [C++ - new delete 高维数组小结](https://blog.csdn.net/weixin_30756499/article/details/97866900)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insert_down1,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* [new和二级指针详解](https://blog.csdn.net/weixin_49638349/article/details/126064770)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insert_down1,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值