SV(Summer Vacation)
尽量简洁。
CF679E
不难发现直接赋值和修改的时间是对的,所以讲一下写法。把将区间变为好数的操作称为加。
对于一个已经进行过区间赋值的区间打一个标记 Up,那么这个区间及其子区间可看作1个点,那么修改的复杂度就跟单点修改是一样的了。
因此对于加操作可以这么写
void do2(int p,ll va){
if(t[p].up){
t[p].dif-=va;t[p].mi=t[p].dif;
while(t[p].dif<0){t[p].dif+=fra[t[p].up+1]-fra[t[p].up];t[p].up++;t[p].mi=t[p].dif;}
}
else t[p].mi-=va,t[p].laz+=va;
return;
}
按时间顺序维护位置比较难做,因为修改的值没有特殊性。
从问题中后缀这个要求入手。
发现如果按位置从后往前,线段树的节点维护当前时间后缀最小值 mx,数量 cnt(如叶子节点1表示1时刻的后缀最小值),
可以发现一个区间的 mx 改变时 cnt++,于是使用无区间加吉司机线段树维护即可。
complete code
CF1588F
对于交换操作,可能合成环或拆开环,导致线段树比较难维护。所以我们考虑根号复杂度的方法。
设我们每 B 个操作进行一次处理。
我们把二操作和三操作的点设为关键点,将一个关键点到下一个关键点中间的链压缩为一个节点,则总节点数在 B 数量级。
对于二操作,一次是 B 数量级。把 B 设为 \(\sqrt{n}\) 即可。
complete code
CUTPLANT
我们称剪后花的高度为 B,剪前花的高度为 A,按 B 从小到大处理,可以发现当两个相同的 B 之间若 maxB<=B 且 minA>=B,则这次剪花可以一起完成。
线段树维护一下即可。
#include<cstdio>
#include<iostream>
#include<algorithm>
#pragma GCC optimize(3,"inline","Ofast")
using namespace std;
const int INF=2e9;
const int MAXN=1e5+5;
typedef pair<int,int> kk;
struct SG{
int b,id;
}ren[MAXN];
int T,ans,n,a[MAXN],t1[MAXN<<3],t2[MAXN<<3];bool pd;
bool cmp(SG xx,SG yy){return xx.b==yy.b?xx.id<yy.id:xx.b<yy.b;}
void pup(int p){t1[p]=min(t1[p<<1],t1[p<<1|1]);t2[p]=max(t2[p<<1],t2[p<<1|1]);}
void build(int p,int l,int r){
if(l==r){t1[p]=a[l];t2[p]=ren[l].b;return;}
int mid=(l+r)>>1;build(p<<1,l,mid);build(p<<1|1,mid+1,r);pup(p);
}
kk que(int p,int L,int R,int l,int r){
if(l>R||r<L)return kk(INF,-INF);
else if(l>=L&&r<=R){return kk(t1[p],t2[p]);}
else{
kk xx,yy;int mid=(l+r)>>1;
xx=que(p<<1,L,R,l,mid);yy=que(p<<1|1,L,R,mid+1,r);
return kk(min(xx.first,yy.first),max(xx.second,yy.second));
}
}
int main(){
scanf("%d",&T);
while(T--){
scanf("%d",&n);ans=0;pd=0;
for(int i=1;i<=n;i++) scanf("%d",&a[i]);
for(int i=1;i<=n;i++){
scanf("%d",&ren[i].b);ren[i].id=i;
if(ren[i].b>a[i]){pd=1;}
}
if(pd){printf("-1\n");continue;}
build(1,1,n);sort(ren+1,ren+1+n,cmp);
for(int i=1;i<=n;i++){
int j=i,B=ren[i].b;int tmp=0;
while(ren[j+1].b==ren[j].b){j++;if(j==n) break;}
for(int k=i;k<=j;k++){
if(tmp==0) tmp=(a[ren[k].id]==ren[k].b)?0:1;
else{
kk ttt=que(1,ren[k-1].id,ren[k].id,1,n);
if(ttt.first>=B&&ttt.second<=B) continue;
else ans++,tmp=(a[ren[k].id]==ren[k].b)?0:1;
}
}
if(tmp) ans++;
i=j;
}
printf("%d\n",ans);
}
return 0;
}
想想为什么 b 是一个排列并且每次只加1,发现我们记录区间 minb 和 maxa,当 maxa>minb 时单点修改即可。
#include<cstdio>
#include<cmath>
#include<iostream>
#include<algorithm>
using namespace std;
const int MAXN=1e5+5;
int n,m,b[MAXN];
void read(int &x){
x=0;
int f=1;
char s=getchar();
while(s<'0'||s>'9'){
if(s=='-') f=-1;
s=getchar();
}
while(s>='0'&&s<='9'){
x=(x<<3)+(x<<1)+(s^48);s=getchar();
}
x*=f;
}
char s[10];
struct SG{
struct tree{
int ma,sum,ta,mb;
}t[MAXN<<2];
void pup(int p){
t[p].sum=t[p<<1].sum+t[p<<1|1].sum;
t[p].ma=max(t[p<<1].ma,t[p<<1|1].ma);
t[p].mb=min(t[p<<1].mb,t[p<<1|1].mb);
return;
}
void pdo(int p){
if(!t[p].ta) return;
t[p<<1].ma+=t[p].ta;t[p<<1|1].ma+=t[p].ta;t[p<<1].ta+=t[p].ta;t[p<<1|1].ta+=t[p].ta;t[p].ta=0;
}
void bui(int p,int l,int r){
t[p].sum=0;t[p].ma=0;t[p].ta=0;
if(l==r){
t[p].mb=b[l];return;
}
int mid=l+r>>1;
bui(p<<1,l,mid);bui(p<<1|1,mid+1,r);pup(p);
}
void upd(int p,int ql,int qr,int l,int r){
if(l>=ql&&r<=qr){
t[p].ma++;
if(t[p].ma<t[p].mb){
t[p].ta++;return;
}
if(l==r&&t[p].ma>=t[p].mb){
t[p].mb+=b[l];t[p].sum++;return;
}
}
pdo(p);
int mid=l+r>>1;
if(ql<=mid) upd(p<<1,ql,qr,l,mid);
if(qr>mid) upd(p<<1|1,ql,qr,mid+1,r);
pup(p);
}
int que(int p,int ql,int qr,int l,int r){
if(l>=ql&&r<=qr){return t[p].sum;}
pdo(p);
int mid=l+r>>1,res=0;
if(ql<=mid) res+=que(p<<1,ql,qr,l,mid);
if(qr>mid) res+=que(p<<1|1,ql,qr,mid+1,r);
return res;
}
}T;
int main(){
while(~scanf("%d%d",&n,&m)){
for(int i=1;i<=n;i++) read(b[i]);
T.bui(1,1,n);
while(m--){
scanf("%s",s+1);
if(s[1]=='a'){
int l,r;read(l);read(r);T.upd(1,l,r,1,n);
}
else{
int l,r;read(l);read(r);printf("%d\n",T.que(1,l,r,1,n));
}
}
}
return 0;
}
0,1赋成-1,1,将相邻前缀和的值连边,不难看出一次操作是将一个环的边反向,那么直接把图看成无向图,每次通过判断 now 和 now-1 的出边数量决定走的方向即可。
UOJ164
对于这类没有区间和的历史最值问题我们可以定义一个标记 (a,b) 表示 x=max(x+a,b)。
在进行修改时对标记进行合并和取 max 即可。
注意:标记之间没有交换律!
complete code
HumanWisdom,不难想到 dep[i]+dep[i+1]-2*dep[lca(i,i+1)]=d[i]
然后考虑倍增,设 va[k]=\(2^k\),因为 \(dep[i]\mod va[k]+dep[i+1]\mod va[k]-2*(dep[lca(i,i+1)]\mod va[k-1])=d[i]\mod va[k]\)
又知道 dep[1]=0,所以从低到高递推即可,判断无解平凡。
complete code
NOI2015 寿司晚宴
因为仅有8个小于 \(\sqrt500\) 的质数。考虑状压,设 dp[i][s1][s2] 表示到前i个数,第一个人选的集合为 s1,第二个人选的集合为 s2。
设 i 的质因数集合为 s,则
dp[i][s1|s][s2]+=dp[i][s1][s2],(s&s2==0)
dp[i][s1][s2|s]+=dp[i][s1][s2],(s&s1==0)
1-n 中的数有且只有一个大于 \(\sqrt500\) 的质因子,把这个因数称作 MAX。
那么把 MAX 相同的数放在一起,这些数只能归属一边(或者不被选)。
所以我们把 dp 分成 f1,f2 来处理即可。
注意有这些数全不被选的情况,这时的 dp 就表示这种情况,所以 dp=f1+f2-dp
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
#define ll long long
const int MAXN=500;
const int MAXM=256;
int n,Mod,f2[MAXM+5][MAXM+5],p[10]={0,2,3,5,7,11,13,17,19,0};
ll Ans,dp[MAXM+5][MAXM+5],f1[MAXM+5][MAXM+5];
struct ren{int ma,va;bool operator<(const ren &a)const{return ma<a.ma;};}a[MAXN+5];
int main(){
scanf("%d%d",&n,&Mod);
for(int i=2;i<=n;i++){
int now=i;a[i].ma=-1;
for(int j=1;j<=8;j++){
if(now%p[j]==0){
a[i].va|=(1<<(j-1));
while(now%p[j]==0){
now/=p[j];
}
}
if(now!=1) a[i].ma=now;
}
}
sort(a+2,a+1+n);
dp[0][0]=1;a[1].ma=0;
for(int i=2;i<=n;i++){
if(i==2||a[i].ma!=a[i-1].ma||a[i].ma==-1){
for(int j=0;j<MAXM;j++){
for(int k=0;k<MAXM;k++){
f1[j][k]=dp[j][k];f2[j][k]=dp[j][k];
}
}
}
for(int j=MAXM-1;j>=0;j--){
for(int k=MAXM-1;k>=0;k--){
if((j&k)!=0) continue;
if((a[i].va&k)==0) f1[j|a[i].va][k]=(f1[j][k]+f1[j|a[i].va][k])%Mod;
if((a[i].va&j)==0) f2[j][k|a[i].va]=(f2[j][k|a[i].va]+f2[j][k])%Mod;
}
}
if(i==n||a[i].ma!=a[i+1].ma||a[i].ma==-1){
for(int j=0;j<=MAXM;j++){
for(int k=0;k<MAXM;k++){
if((j&k)!=0) continue;
dp[j][k]=(((f1[j][k]+f2[j][k]-dp[j][k])%Mod)+Mod)%Mod;
}
}
}
}
for(int i=0;i<MAXM;i++){
for(int j=0;j<MAXM;j++){
if((i&j)==0) Ans=(Ans+dp[i][j])%Mod;
}
}
printf("%lld",Ans);return 0;
}
NOI2009 管道取珠
这个 \(a_i^2\) 导致我们不管对于产生方式的数量还是每种数量对应的产生方式都不好计算。
考虑转化意义,假设现在有两个管道,若我们设 \(b_i\) 表示管道1,2都得到 i 序列的方案数,发现 \(b_i=a_i^2\)
那么就变成了简单的传纸条问题了。
NOI2009 二叉查找树
BST 有中序遍历一定的性质。考虑设 dp[i][j][k] 表示中序遍历为 [i,j] 的节点权值>=k的方案数。
我们每次取出 \(p\in[i,j]\) 为根节点,这样做会使 \([i,j]\) 的节点深度+1,那么贡献就是访问频率之和了。
因为原始树上子节点权值已经大于 val[p],故只需判断 val[p] 与 k 大小关系即可。
NOI2014 购票
可以轻松地想到树剖加线段树套李超树的做法,但是时间复杂度有3个 log。
于是可以考虑可撤销李超树,这个跟可撤销并查集一样用栈记录一下即可,复杂度降为两个 log,但是空间还是有点卡。
反正能过就行🤭
[省选联考 2020 A 卷] 树
不可能每次跑完子树,所以尝试把答案记录在子节点。
我们需要处理所有权值+1后异或和的结果,注意到若 \(x=2^i-1\mod 2^i\),则+1后 x 第 i 位取反。
考虑按位操作,设 va[i]=va[i]+dep[i],当前点为 x,则我们需要找到 \(va[i]-dep[x]-1=2^j-1\mod2^j\) 的 i 有多少个
化简即为 \(va[i]=dep[x]\) 的个数。
开桶记录然后 dsu on tree 即可。
[省选联考 2021 A/B 卷] 滚榜
\(n\leq13\) 想到状压,设 \(dp_{s,i,j,k}\) 表示当前集合为 s,最后一个人为 i,b[i]=j,使用的 b 的和为 k。
这样时间复杂度是 \(\Omicron(2^nn^2m^2)\)
观察发现对于每种公布顺序,我们不关心 b 的具体分配。
因为我们可以贪心地使当前的人,花最少的代价超过上一个,结束后有多余的 b 全往后面堆就行。
设 \(dis_{i,j}\) 表示 j 要超过 i 的最小代价,可以发现 \(dis_{i,j}=\max(0,a_i-a_j+(i<j))\)
又因为 b 不降,设 c 为 b 的差分数组,不难发现 \(\sum_{i=1}^nc_i\times(n-i+1)=\sum_{i=1}^nb_i\)
这样我们就不用表示 b[i] 了。
对于上一个数为 j,当前数为 i,则
\(dp_{s,i,k}+=dp_{s^(1<<(j-1)),j,k-(n-con_s+1)\times dis_{j,k}}\)
有了以上两点,我们把 dis 当作 c,然后费用提前计算即可。
CF626F
把一个人作为最小值贡献为-\(a_i\),作为最大值贡献为\(a_i\),放在中间没有贡献。
我们设 \(dp_{i,j,k}\) 表示前 i 个人,有 j 组未结束,当前贡献为 k。
注意自己单独一个组和放中间的转移是一样的,所以是 \(dp_{i,j,k}+=dp_{i-1,j,k}\)
不妨记 c 为 a 排序后的差分数组。
我们新加入一个数后,对于未结束的组,后面加入的 maxa>=a[i],而这些组的 mina<=a[i],所以maxa-mina 的插值一定包含了 c[i]。
我们也采用费用提前计算就完事了。
CQOI2015 选数
若子题。
首先不难想到只提取[L,R]中 k 的倍数,设 l=\(\frac{L-1}{k}\)+1,r=\(\frac{R}{k}\)
因为(a,b)和(b,a)算两种方案,所以求答案的式子就是
套路莫反加杜教筛即可。
本文来自博客园,作者:{StranGePants},转载请注明原文链接:https://www.cnblogs.com/StranGePants/p/17537355.html