(根号分治、sosdp)洛谷 P10408 Apple / P7842 探险者笔记 III 题解
原因是打 GJOI 10.24(模拟赛 27)时,做到了第二题,讲题的大佬说,同校的大佬曾经出的模拟赛就是一道模板题目。
首先学会找 sss 的子集:
for(int sub=s;sub;sub=s&(sub-1))
原理是每次把末位的 1 砍掉,如果是 0 也有退位。
还有限定了 bbb 位二进制位下的超集:
all=1<<b;
...
ll buj=all^s;
for(int sub=buj;sub;sub=buj&(sub-1))
s|sub---------------------------
for(int i=s;i<all;i=(i+1)|s)
原理:给 sss 的每一位 0 填入 1,这相当于关于全集取反得到补集,然后枚举补集的子集,或上 sss 得到超集。也可以用下面的写法,和求子集的方法同理。
1.Apple
题意
LAR 有 2n2^n2n 个苹果,苹果用 000 到 2n−12^n - 12n−1 编号,编号为 iii 的苹果的价值是 viv_ivi。
如果 AorB=AA\operatorname{or}B=AAorB=A,那么可以说 AAA 包含 BBB(or\operatorname{or}or 是按位或)。
因为 LAR 的苹果太多了,所以他不知道如何挑选苹果。他想进行一些操作,方便他吃苹果。
总共有两种操作,共 qqq 个操作:
- 1 S1\ S1 S ,询问所有编号被 SSS 包含的苹果的价值总和。
- 2 S A2\ S\ A2 S A ,改变编号为 SSS 的苹果的价值为 AAA(将 vSv_SvS 改为 AAA)。
1≤n≤201\le n \leq 201≤n≤20 ,1≤q≤3×1051 \le q\leq3\times10^51≤q≤3×105,0≤vi≤231−10\leq v_i\leq 2^{31}-10≤vi≤231−1 。
思路
如果只有查询,就是高维前缀和(sosdp)模板。要改变 sss 的价值,那么 sss 的超集就要同步更新,枚举超集需要 O(2n)O(2^n)O(2n)。
于是大佬就讲了一个很高级的做法——高低位分治。即令 b=n2b=\frac{n}{2}b=2n,将 sss 的前 bbb 位和后 bbb 位分开:
- 对于前 bbb 位,都对后 bbb 位做高维前缀和;
- 修改 sss 以及超集,只更新 sss 后 bbb 位的超集;
- 查询 sss 的子集,查询 sss 前 bbb 位的所有子集携带的后 bbb 位子集和。
因为折半,所以开头预处理初始高维前缀和时间复杂度为 O(2n)O(2^n)O(2n),修改和查询都是 O(2b)O(2^b)O(2b),时间复杂度来到 O(2n+2bq)O(2^n+2^bq)O(2n+2bq)。
为什么这样就是对的?
- 对于查询的 sss 拆成 hishi_shis 和 lwslw_slws,对 hishi_shis 的子集、对应 lwslw_slws 子集和 flwsf_{lw_s}flws 求和,就覆盖了所有子集;
- 假设 sss 的某个子集 subsubsub 的值在查询之前被修改,根据做法就是更新 hisubhi_{sub}hisub 下的 lwsublw_{sub}lwsub 超集,因为 lwsub⊆lwslw_{sub}\subseteq lw_slwsub⊆lws,所以 hisubhi_{sub}hisub 下 flwsf_{lw_s}flws 被更新;
- 查询 sss 的时候,hishi_shis 的子集有 hisubhi_{sub}hisub,hisubhi_{sub}hisub 下的 flwsf_{lw_s}flws 会被算进贡献。
于是这是正确的。这其实是高维前缀和直接包含了所有子集这一性质的妙用。具体细节见代码。
代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=21,B=10,S=(1<<B),Sa=(1<<N);
ll n,Q,all;
ll b,a[Sa];
struct sos
{ll f[S];void init(){for(int i=0;i<b;i++)for(int mask=0;mask<(1<<b);mask++)if(mask&(1<<i))f[mask]+=f[mask^(1<<i)];}void add(ll s,ll k){ll buj=all^s;for(int sub=buj;sub;sub=buj&(sub-1))f[s|sub]+=k;f[s]+=k;}
}U[S];
int main()
{scanf("%lld%lld",&n,&Q);b=n/2;all=(1<<b)-1;for(ll i=0;i<(1<<n);i++){scanf("%lld",&a[i]);ll hi=(i>>b),lw=i^(hi<<b);U[hi].f[lw]=a[i];}for(int i=0;i<(1<<b);i++)U[i].init();while(Q--){ll op,s,x;scanf("%lld%lld",&op,&s);ll hi=(s>>b),lw=s^(hi<<b);if(op==2){scanf("%lld",&x);U[hi].add(lw,x-a[s]);a[s]=x;}else {ll ret=0;for(int sub=hi;sub;sub=hi&(sub-1))ret+=U[sub].f[lw];ret+=U[0].f[lw];printf("%lld\n",ret);}}return 0;
}
2.探险者笔记 III
题意
改制后的《探险者笔记》由 nnn 个关卡组成,每个关卡有一个难度 bib_ibi,同时有 mmm 个成就,第 iii 个成就需要你恰好完成 sumisum_isumi 个关卡,且刚好分别是 ai1,ai2,...,aisumia_{i_1},a_{i_2},...,a_{i_{sum_i}}ai1,ai2,...,aisumi。完成第 iii 个成就可以得到 viv_ivi 的分数。
如果长时间推关而没有获得任何成就,小 Soup 会感到疲乏。而且成就的解锁是有一定顺序的。因此上一个获得第 iii 个成就接下来再获得第 jjj 个成就的条件是 i<ji<ji<j 且 w+∑k=1sumibaik≥∑k=1sumjbajkw+\sum\limits_{k=1}^{sum_i}b_{a_{i_k}}\ge\sum\limits_{k=1}^{sum_j}b_{a_{j_k}}w+k=1∑sumibaik≥k=1∑sumjbajk,其中 www 是一个给定的常数。
第一次获得成就没有任何限制。求最多他能得到多少分数。
1≤n≤18,1≤m≤105,1≤sumi≤18,1≤w,bi,vi≤103,1≤ai≤n1\le n\le18,1\le m\le10^5,1\le sum_i\le18,1\le w,b_i,v_i\le10^3,1\le a_i\le n1≤n≤18,1≤m≤105,1≤sumi≤18,1≤w,bi,vi≤103,1≤ai≤n。
思路
注意题意,后面的成就能够达成,需要前面做的任务是后面要求的子集。
记 sumisum_isumi 表示成就 iii 的难度总和,stist_isti 表示任务 iii 代表的任务状态,设 fif_ifi 表示,以成就 iii 作为最后一个,能够完成的最大成就数量。有转移:
fi=maxj<i,w+sj≥si,stj⊆stifj+1f_i=\max_{j<i,w+s_j\ge s_i,st_j\subseteq st_i}{f_j}+1fi=j<i,w+sj≥si,stj⊆stimaxfj+1
发现这个转移条件很偏序啊,是可以用 cdq 维护的。怎么维护第三个偏序条件呢?一般来说三维偏序的第三维是放到树状数组上维护,这里维护子集的 dp 最大值就可以用 sosdp 维护。
但是鉴于这里二进制位有 181818 位,我们考虑用上面的高低位分治进行维护。
然后就是经典的 cdq 分治优化 dp 了:先遍历左区间,处理左区间对右区间的贡献(满足一重偏序),然后再遍历右区间。时间复杂度 O(2bmlog2m)O(2^{b}m\log^2 m)O(2bmlog2m),这里 b=⌈n2⌉b=\left\lceil\frac{n}{2}\right\rceilb=⌈2n⌉。
代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=20,M=1e5+9,S=1<<10;
ll n,m,w;
ll b,all;
ll bb[N];
struct award
{ll val;ll st,sum;
}a[M];
ll id[M];
bool cmp(ll x,ll y)
{return a[x].sum>a[y].sum;
}
struct sos
{ll f[S];void add(ll s,ll k){ll buj=all^s;for(ll sub=buj;sub;sub=buj&(sub-1))f[s|sub]=max(f[s|sub],k);f[s]=max(f[s],k);}void clean(ll s){ll buj=all^s;for(ll sub=buj;sub;sub=buj&(sub-1))f[s|sub]=0;f[s]=0;}
}U[S];
struct BUT
{void add(ll s,ll k){ll hi=(s>>b),lw=s^(hi<<b);U[hi].add(lw,k);}ll query(ll s){ll hi=(s>>b),lw=s^(hi<<b);ll ret=U[0].f[lw];for(ll sub=hi;sub;sub=hi&(sub-1))ret=max(ret,U[sub].f[lw]);return ret;}void clean(ll s){ll hi=(s>>b),lw=s^(hi<<b);U[hi].clean(lw);}
}B;
ll F[M],ans;
void cdq(ll l,ll r)
{if(l>=r){F[id[l]]=max(F[id[l]],a[id[l]].val);ans=max(ans,F[id[l]]);return;}ll mid=(l+r)>>1;cdq(l,mid);sort(id+l,id+mid+1,cmp);sort(id+mid+1,id+r+1,cmp);ll p=l,q=mid+1;while(p<=mid&&q<=r){if(a[id[p]].sum+w>=a[id[q]].sum){//	cout<<id[p]<<"->"<<id[q]<<endl;B.add(a[id[p]].st,F[id[p]]);p++;}else {F[id[q]]=max(F[id[q]],B.query(a[id[q]].st)+a[id[q]].val);ans=max(ans,F[id[q]]);q++;}}while(p<=mid){B.add(a[id[p]].st,F[id[p]]);p++;}while(q<=r){F[id[q]]=max(F[id[q]],B.query(a[id[q]].st)+a[id[q]].val);ans=max(ans,F[id[q]]);q++;}for(int o=l;o<=mid;o++)B.clean(a[id[o]].st);for(int o=l;o<=r;o++)id[o]=o;cdq(mid+1,r);
}
void _2(ll x)
{ll w[N],tot=0;while(x){w[++tot]=(x&1);x>>=1;}for(int i=tot;i>=1;i--)cout<<w[i]<<" ";puts(""); 
}
int main()
{scanf("%lld%lld%lld",&n,&m,&w);b=n/2;for(int i=0;i<n;i++)scanf("%lld",&bb[i]);all=(1<<(n-b))-1;for(int i=1;i<=m;i++){ll x,m;id[i]=i;scanf("%lld%lld",&a[i].val,&m);for(int j=1;j<=m;j++){scanf("%lld",&x);a[i].st|=(1<<(x-1));a[i].sum+=bb[x-1];}//	printf("sum:%lld state:",a[i].sum); //	_2(a[i].st);}cdq(1,m);
//	for(int i=1;i<=m;i++)
//	cout<<F[i]<<" ";
//	cout<<endl;printf("%lld",ans);return 0;
}
