CSP复习
一、tarjan
1.有向图缩点(强连通分量)
2.割点(根节点且有至少两个孩子 或 low[v]>=dfn[u])
if((fa==0&&son>1)||(fa!=0&&dfn[u]<=low[v]))cut[u]=true;
3.边双(两条边算一条,记一个vis标记一下有没有经过)
P2860 [USACO06JAN]冗余路径Redundant Paths
求边双通常有两种方法:记住前驱节点,或是记住前驱边 。
注意重边问题,有些题只能记住前驱节点,有些题只能记住记住前驱边,不同的题不同对待。
记住前驱节点
for(int i=head[u];i;i=edge[i].next)
{
int v=edge[i].too;
if(v==fa)continue;
if(!dfn[v]){tarjan(v,u);low[u]=min(low[u],low[v]);}
else low[u]=min(low[u],dfn[v]);
} 记住前驱边
for(int i=head[u];i;i=edge[i].next)
if(!vis[i])
{
vis[i]=vis[i^1]=1;
int v=edge[i].too;
if(!dfn[v]){tarjan(v);low[u]=min(low[u],low[v]);}
else low[u]=min(low[u],dfn[v]);
} 4.点双(删掉任意一个节点仍是连通图)
void tarjan(int u)
{
dfn[u]=low[u]=++deep;
zhan[++tot]=u;
for(int i=head[u];i;i=edge[i].next)
{
int v=edge[i].too;
if(!dfn[v])
{
tarjan(v);
low[u]=min(low[u],low[v]);
if(low[v]>=dfn[u])
{
cnt++;
while(zhan[tot]!=v)
{
add2(cnt,zhan[tot]);
add2(zhan[tot],cnt);
tot--;
}
tot--;
add2(cnt,v);
add2(v,cnt);
add2(cnt,u);
add2(u,cnt);
}
}
else low[u]=min(low[u],dfn[v]);
}
}
void dfs(int u,int fa)
{
if(u<=n)size[u]++;
for(int i=head2[u];i;i=edge2[i].next)
{
int v=edge2[i].too;
if(v==fa)continue;
dfs(v,u);
size[u]+=size[v];
}
if(u<=n)
{
f[u]=2*(n-1);
for(int i=head2[u];i;i=edge2[i].next)
{
int v=edge2[i].too;
if(v==fa)continue;
f[u]+=size[v]*(n-1-size[v]);
}
f[u]+=(n-size[u])*(size[u]-1);
}
} 二、2-sat
1.模板【模板】2-SAT 问题
0表示不选,1表示选
1.a=0 , a->a'
2.a=1 , a'->a
3.a=1那么b=1 , a->b b'->a'(隐藏b=0那么a=0)
4.a=0那么b=0 , a'->b' b->a(隐藏b=1那么a=1)
5.a=1那么b=0 , a->b' b->a'(隐藏b=1那么a=0)
6.a=0那么b=1 , a'->b b'->a(隐藏b=0那么a=1)
7.a=0并且b=0 a=0并且b=1 a=1并且b=0 a=1并且b=1 参考3-6,进行合并
8.a^b=0 a^b!=0 参考3-6,进行合并
如果col[a]==col[a'],那么无解。
关于求一组解,由于我们知道tarjan的dfn序其实就是拓扑的反序,所以当col[a]<col[a']时,a=1。
void tarjan(int u)
{
deep++;dfn[u]=low[u]=deep;
vis[u]=true;zhan[++tot]=u;
for(int i=head[u];i;i=edge[i].next)
{
int v=edge[i].too;
if(!dfn[v]){tarjan(v);low[u]=min(low[u],low[v]);}
else if(vis[v])low[u]=min(low[u],low[v]);
}
if(dfn[u]==low[u])
{
color++;vis[u]=false;col[u]=color;
while(zhan[tot]!=u)
{
vis[zhan[tot]]=false;
col[zhan[tot]]=color;
tot--;
}
tot--;
}
}
bool TWO_SAT()
{
for(int i=1;i<=2*n;i++)
if(!dfn[i])tarjan(i);
for(int i=1;i<=n;i++)
if(col[i]==col[i+n])return false;
return true;
}
signed main()
{
n=read();m=read();
for(int i=1;i<=m;i++)
{
int a=read(),x=read(),b=read(),y=read();
if(x==0)
{
if(y==0)//a=1那么b=0(b=1那么a=0)
{
add(a,b+n);
add(b,a+n);
}
else{//a=1那么b=1(b=0那么a=0)
add(a,b);
add(b+n,a+n);
}
}
else{
if(y==0)//a=0那么b=0(b=1那么a=1)
{
add(a+n,b+n);
add(b,a);
}
else{//a=0那么b=1(b=0那么a=1)
add(a+n,b);
add(b+n,a);
}
}
}
if(TWO_SAT())
{
puts("POSSIBLE");
for(int i=1;i<=n;i++)
printf("%d ",col[i]<col[i+n]);
}
else printf("IMPOSSIBLE");
return 0;
} 2.前缀优化建图
区间,我们看成区间
和
如果区间那么这个物品一定不选,如果
,
那么这个物品一定选
signed main()
{
np=read();n=read();m=read();mp=read();
for(int i=1;i<=np;i++)//x=0那么y=1,y=0那么x=1
{
int x=read(),y=read();
add(x+n,y);
add(y+n,x);
}
for(int i=2;i<=m;i++)//x=0那么y=0,y=1那么x=1
{
add(n+n+i+m,n+n+i-1+m);
add(n+n+i-1,n+n+i);
}
for(int i=1;i<=n;i++)
{
int x=read(),y=read();
if(x>1)
{
add(i,n+n+m+x-1);//x=1那么y=0,
add(n+n+x-1,n+i);//y=1那么x=0,
}
add(i,n+n+y);//x=1那么y=1,
add(n+n+m+y,n+i);//y=0那么x=0,
}
for(int i=1;i<=mp;i++)//x=1那么y=0,y=1那么x=0
{
int x=read(),y=read();
add(x,y+n);
add(y,x+n);
}
for(int i=1;i<=n+n+m+m;i++)
if(!dfn[i])tarjan(i);
for(int i=1;i<=n;i++)
if(col[i]==col[i+n]){puts("-1");return 0;}
for(int i=1;i<=m;i++)
if(col[n+n+i]==col[n+n+m+i]){puts("-1");return 0;}
for(int i=1;i<=n;i++)
if(col[i]<col[i+n])ans++;
for(int i=1;i<=m;i++)
if(col[n+n+i]<col[n+n+m+i])
{
printf("%d %d\n",ans,i);
break;
}
for(int i=1;i<=n;i++)
if(col[i]<col[i+n])printf("%d ",i);
} 三、Floyd
1.两点之间的距离
for(int i=1;i<=n;i++)f[i][i]=0;
for(int k=1;k<=n;k++)
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
f[i][j]=min(f[i][j],f[i][k]+f[k][i]); 2.判负环
if(f[i][i]<0){puts("-1");return 0;} 3.DP
表示以前k个数做中转点,i到j的最短路。
按时间顺序排列,对于时间<=T的点都可以做中转点。
now=1;
for(int i=1;i<=m;i++)
{
scanf("%d%d%d",&x,&y,&z);
x++;y++;
while(a[now]<=z&&now<=n)
{
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
g[i][j]=min(g[i][j],g[i][now]+g[j][now]);
now++;
}
if(a[x]>z||a[y]>z)cout<<-1<<endl;
else {
if(g[x][y]==1e8)cout<<-1<<endl;
else cout<<g[x][y]<<endl;
}
} 4.传递闭包
在可以走回头路的状压DP中需要用到传递闭包。
memset(f,0x3f,sizeof(f));
memset(dp,0x3f,sizeof(dp));
f[0][1]=0;
for(int i=1;i<(1<<n);i++)
{
int gs=0;
for(int j=0;j<n;j++)
if(i&(1<<j))
{
gs++;
id[gs]=j;
}
for(int p=1;p<=gs;p++)
for(int j=1;j<=gs;j++)
A[j][p]=g[id[p]][id[j]];
for(int j=1;j<=gs;j++)A[j][j]=0;
for(int k=1;k<=gs;k++)
for(int p=1;p<=gs;p++)
for(int j=1;j<=gs;j++)
A[p][j]=min(A[p][j],A[p][k]+A[k][j]);
for(int p=1;p<=gs;p++)
for(int j=1;j<=gs;j++)f[id[p]][i]=min(f[id[p]][i],f[id[j]][i]+A[j][p]);
for(int j=0;j<n;j++)
if((i&(1<<j))&&f[j][i]!=0x3f3f3f3f)
{
for(int k=0;k<n;k++)
if(((i&(1<<k))==0)&&g[j][k]!=0x3f3f3f3f)
f[k][i+(1<<k)]=min(f[k][i+(1<<k)],f[j][i]+g[j][k]);
}
} 5.最小环
i和j两点加上中转点k,构成一个环
for(int i=1;i<=n;i++)
f[i][i]=e[i][i]=0;
for(int k=1;k<=n;k++)
{
for(int i=1;i<k;i++)
for(int j=i+1;j<k;j++)
ans=min(ans,f[i][j]+e[j][k]+e[k][i]);
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
f[i][j]=min(f[i][j],f[i][k]+f[k][j]);
} 四、三分和随机(模拟退火)
三分和模拟退火都是解决极值类问题。
三分用来解决函数极值。
模拟退火还可以解决一些不是函数关系,往往要选出一些东西的题目。
1.三分
double l=0.0,r=n;
while(r-l>=1e-3)
{
double mid1=l+(r-l)/3.0;
double mid2=(r)-(r-l)/3.0;
if(suan(mid1)<suan(mid2))r=mid2;
else l=mid1;
} 2.模拟退火
精度有些问题,所以退火后再前后枚举一下
为了使答案更加准确,我们可以先随机若干次,这样的话跳动会更加优秀。
for(int i=1;i<=3000;i++)
{
int X=rand()%n+1,Y=rand()%n+1;
int ret=calc(X,Y);
if(ret>ans)ans=ret,x=X,y=Y;
} void tui()
{
t=1000;
while(t>1e-12)
{
X=x+(rand()*2-RAND_MAX)*t*0.0001;
if(X<0)X=0;if(X>1000)X=1000;
now=suan(X);
double anss=now-ans2;
if(anss<0.0){ans1=X;ans2=now;x=X;}
else if(exp(-anss/t)*RAND_MAX>rand())x=X;
t=t*delta;
}
}
void solve()
{
ans1=0.0;ans2=suan(x);
tui();
double xu=ans1;
double xjh=xu;
double XJH=0.0;
for(int i=1;i<=1000;i++)
{
xjh+=0.0001;
if(xjh>1000.0)break;
XJH=suan(xjh);
if(XJH<ans2)
{
ans1=xjh;
ans2=XJH;
}
}
xjh=xu;
XJH=0.0;
for(int i=1;i<=1000;i++)
{
xjh-=0.0001;
if(xjh<0.0)break;
XJH=suan(xjh);
if(XJH<ans2)
{
ans1=xjh;
ans2=XJH;
}
}
} 3.多函数
这题是双函数,三分求函数极值的话,需要三分套三分,而且细节多。
要找多个点时,模拟退火就是一个不错的选择。因为模拟退火基本上不用修改,而三分会写自闭。
三分套三分
double sanfen(double x,double y){
double lx=cx,ly=cy,rx=dx,ry=dy;
while(dis(lx,ly,rx,ry)>eps){
double m1x=lx+(rx-lx)/3.0,m2x=rx-(rx-lx)/3.0,m1y=ly+(ry-ly)/3.0,m2y=ry-(ry-ly)/3.0;
double ans1=dis(x,y,m1x,m1y)/r+dis(m1x,m1y,dx,dy)/q;
double ans2=dis(x,y,m2x,m2y)/r+dis(m2x,m2y,dx,dy)/q;
if(ans2-ans1>eps) rx=m2x,ry=m2y;
else lx=m1x,ly=m1y;
}
return dis(x,y,lx,ly)/r+dis(lx,ly,dx,dy)/q;
}
double lx=ax,ly=ay,rx=bx,ry=by;
while(dis(lx,ly,rx,ry)>eps){
double m1x=lx+(rx-lx)/3.0,m2x=rx-(rx-lx)/3.0,m1y=ly+(ry-ly)/3.0,m2y=ry-(ry-ly)/3.0;
double ans1=dis(ax,ay,m1x,m1y)/p+sanfen(m1x,m1y);
double ans2=dis(ax,ay,m2x,m2y)/p+sanfen(m2x,m2y);
if(ans2-ans1>eps) rx=m2x,ry=m2y;
else lx=m1x,ly=m1y;
} 模拟退火
void tui()
{
t=2000;
while(t>1e-14)
{
X=x+(rand()*2-RAND_MAX)*t;
Y=y+(rand()*2-RAND_MAX)*t;
if(X<0)X=0;if(X>dis(ax,bx,ay,by))X=dis(ax,bx,ay,by);
if(Y<0)Y=0;if(Y>dis(cx,dx,cy,dy))Y=dis(cx,dx,cy,dy);
now=suan(X,Y);
anss=now-ans;
if(anss<0){ans=now;x=X;y=Y;}
else if(exp(-anss/t)*RAND_MAX>rand()){x=X;y=Y;}
t=t*delta;
}
} 4.和随机有点关系的题目
这题数据范围一看,发现会20!,但只要求最大那就可以随机。
随机的话两种思路,random_shuffle和模拟退火
怎么模拟退火,每次交换两个数,在用DP求解即可。
inline void tui()
{
nowans=ans,t=2000;
while (t>1e-6)
{
int x=0,y=0;
x=rand()%n+1;y=rand()%n+1;
if(x==y) continue;
swap(a[x],a[y]);
now=DP();
anss=now-ans;
if (anss<0) ans=nowans=now;
else if (exp(-anss/t)*RAND_MAX>rand()) nowans=now;
else swap(a[x],a[y]);
t*=delta;
}
} 可以状压,当然模拟退火一样可以。
五、容斥
容斥的一般式子:
1.简单容斥
就是加加减减,还算简单。
2.和集合有关的容斥
有题了再补
3、minmax容斥
推导详见洛谷日报或GXZlegend
公式:
应用:
常用来求“每次选一个数,使每个数被选的次数至少到达某个值的期望次数”
首先我们要知道对于一个局面(比如第一个数选1次,第二个数选2次)的期望步数就是这个局面的概率分之一。
证明:设期望为
,概率为
,那么
,化简得
为啥是这样的呢?就是有的概率一遍就行,有
的概率再来一次。
普遍情况:
个数,每个数有个权重
,那么每一次选中第
个数的概率就为
,问每个数被选的次数至少到达
的期望次数。
公式:!%7D%7B%5Csum%20c_i!%7D%20%5Cprod%20(%5Cfrac%7Ba%5Bx_i%5D%7D%7B%5Csum%20a%5Bx_i%5D%7D)%5E%7Bd_i%7D&preview=true)
解释:
是得到一个指定集合内元素的期望时间,
有重复元素的排列下,
是对于集合内每个数的概率
例题:
hdu4336]Card Collector
裸题,但也有用到一个状压技巧,和飞翔的小鸟一样。
double ans=0;
for(int i=0;i<n;i++)scanf("%lf",&a[1<<i]);
for(int i=1;i<(1<<n);i++)
{
f[i]=f[i-lowbit(i)]+a[lowbit(i)];
gs[i]=gs[i-lowbit(i)]+1;
}
for(int i=1;i<(1<<n);i++)
if(gs[i]&1)ans+=1.0/f[i];
else ans-=1.0/f[i];
printf("%lf\n",ans); E - Gachapon
题意:
个数,每个数有个权重
,那么每一次选中第
个数的概率就为
,问每个数被选的次数至少到达
的期望次数。
题解:
暴力做法就是枚举集合,然后通过上面给出的式子进行计算。
正解,考虑已经求出了一个集合的答案,现在要在这个集合
里加入一个新的数
发现只要记录一下和
即可转移
代码:
#include<bits/stdc++.h>
using namespace std;
#define next Next
#define gc getchar
#define int long long
const int N=405;
const int mod=998244353;
int n,a[N],b[N],jc[N],inv[N],sum[N],f[N][N][N];
/*char buf[1<<21],*p1=buf,*p2=buf;
inline int gc(){return p1==p2&&(p2=(p1=buf)+fread(buf,1,1<<21,stdin),p1==p2)?EOF:*p1++;}*/
inline int read()
{
int ret=0,f=0;char c=gc();
while(!isdigit(c)){if(c=='-')f=1;c=gc();}
while(isdigit(c)){ret=ret*10+c-48;c=gc();}
if(f)return -ret;return ret;
}
int kuai(int a,int b)
{
int res=1;
while(b)
{
if(b&1)res=res*a%mod;
a=a*a%mod;
b=b/2;
}
return res;
}
signed main()
{
n=read();
for(int i=1;i<=n;i++)a[i]=read(),b[i]=read();
jc[0]=jc[1]=inv[0]=inv[1]=1;
for(int i=2;i<=400;i++)jc[i]=jc[i-1]*i%mod;
for(int i=2;i<=400;i++)inv[i]=(mod-mod/i)*inv[mod%i]%mod;
for(int i=2;i<=400;i++)inv[i]=inv[i]*inv[i-1]%mod;
int s1=0,s2=0;
f[0][0][0]=mod-1;
for(int i=1;i<=n;i++)
{
for(int j=0;j<=s1;j++)
for(int k=0;k<=s2;k++)
f[i][j][k]=f[i-1][j][k];
sum[0]=1;
for(int j=1;j<b[i];j++)sum[j]=sum[j-1]*a[i]%mod;
for(int j=0;j<b[i];j++)sum[j]=sum[j]*inv[j]%mod;
for(int j=0;j<=s1;j++)
for(int k=0;k<=s2;k++)
for(int p=0;p<b[i];p++)
f[i][j+p][k+a[i]]=(f[i][j+p][k+a[i]]-f[i-1][j][k]*sum[p]%mod+mod)%mod;
s1+=b[i]-1;s2+=a[i];
}
int ans=0;
for(int i=0;i<=s1;i++)
for(int j=1;j<=s2;j++)
{
int x=f[n][i][j]*jc[i]%mod;
x=x*kuai(kuai(j,mod-2),i)%mod;
ans=(ans+x*s2%mod*kuai(j,mod-2)%mod)%mod;
}
cout<<ans;
} 六、DP
1.树形DP
背包P2014 选课
for(int i=head[u];i;i=edge[i].next)
{
int v=edge[i].too;
dfs(v);
for(int j=m;j>=1;j--)
{
for(int k=0;k<j;k++)
{
f[u][j]=max(f[u][j],f[v][k]+f[u][j-k]);
}
}
} 最大独立子集旅游
void dfs(int u,int fa)
{
f[u][1]=1;
for(int i=0;i<g[u].size();i++)
{
int v=g[u][i];
if(v==fa)continue;
dfs(v,u);
f[u][1]+=f[v][0];
f[u][0]+=max(f[v][1],f[v][0]);
}
} 2.状压DP
for(int i=2;i<=n;i++)
{
for(int j=1;j<=cnt;j++)
if((xu[j]&S[i-1])==xu[j])
for(int k=1;k<=cnt;k++)
if(b[j][k])
if((xu[k]&S[i])==xu[k])dp[i][k]=(dp[i][k]+dp[i-1][j])%mod;
} 3.数位DP
ll dfs(int len,bool b,int sum,bool zero,int d)//b=0表示前面都是最大的
{
ll ans=0;
if(len==0)return sum;
if(f[len][b][sum][zero]!=-1)return f[len][b][sum][zero];
for(int i=0;i<=9;i++)
{
if(b==false && i>num[len]) break;
ans=ans+dfs(len-1,(b==true)||(i<num[len]),sum+(((zero!=1)||(i!=0))&&(i==d)),zero&&(i==0),d);
}
f[len][b][sum][zero]=ans;
return ans;
} 4.概率期望DP
题目:给出一个n×m的矩阵区域,一个机器人初始在第x行第y列,每一步机器人会等概率的选择停在原地,左移一步,右移一步,下移一步,如果机器人在边界则不会往区域外移动,问机器人到达最后一行的期望步数。
题解:
这题很难啊,50分的要用期望+高斯消元。
我们先设为从(i,j)到最后一行的期望步数
f[i][j]=f[i][j]/4+f[i][j-1]/4+f[i][j+1]/4+f[i+1][j]/4+1 (1<j<m) f[i][j]=f[i][j]/3+f[i+1][j]/3+f[i][j+1]/3+1 j=1 f[i][j]=f[i][j]/3+f[i+1][j]/3+f[i][j-1]/3+1 j=m
初始化是什么?
f[n][i]=0
两维的数组不好,我们可以压掉第一维(比较好高斯消元)。
然后,我们化一下式子:
f[i]=f[i]/3+f[i+1]/3+last[i]/3+1 2/3 * f[i]=f[i+1]/3+last[i]/3+1 f[i]=f[i+1]/2+last[i]/2 + 3/2 f[i]-f[i+1]/2=last[i]/2 + 3/2 (i=1)
f[i]=f[i]/3+f[i-1]/3+last[i]/3+1 2/3 * f[i]=f[i-1]/3+last[i]/3+1 f[i]=f[i-1]/2+last[i]/2 + 3/2 f[i]-f[i-1]/2=last[i]/2 + 3/2 (i=m)
f[i]=f[i]/4+f[i-1]/4+f[i+1]/4+last[i]/4+1 3/4 * f[i]=f[i-1]/4+f[i+1]/4+last[i]/4+1 f[i]=f[i-1]/3+f[i+1]/3+last[i]/3 + 4/3 f[i]-f[i-1]/3-f[i+1]/3=last[i]/3 + 4/3 (1<i<m)
这不就是高斯消元吗,对每一行做一遍高斯消元,把答案记录在last数组里。
我们发现高斯矩阵的系数很小,不需要n^3枚举,只要手动解就好。
5.子集DP
详见我牛客题解SOS-DP(子集DP)
for(int i=0;i<n;i++)
for(int j=0;j<(1<<n);j++)
if(j&(1<<i))F[j]+=F[j^(1<<i)]; 6.换根DP
for(int i=head[u];i;i=edge[i].next)
{
int v=edge[i].too;
if(v==fa)continue;
for(int j=0;j<A[v].size();j++)
{
int x=b[v][j],y=A[v][j];
x=gcd(x,a[u]);
int z=lower_bound(b[u].begin(),b[u].end(),x)-b[u].begin();
A[u][z]-=y;
}
for(int j=0;j<A[u].size();j++)
{
int x=b[u][j],y=A[u][j];
x=gcd(x,a[v]);
int z=lower_bound(b[v].begin(),b[v].end(),x)-b[v].begin();
A[v][z]+=y;
}
dp(v,u);
A[u]=B[u];
} 7.单调队列优化DP
for(int i=1;i<=n;i++)
{
while(x[j]+L<=x[i])
{
if(f[j]>-1e8)
{
while(l<=r&&f[j]>=f[q[r]])r--;
q[++r]=j;
}
j++;
}
while(x[q[l]]+R<x[i])l++;
if(l<=r)f[i]=f[q[l]]+s[i];
} 8.数据结构优化DP
题目:定义一个长度为奇数的区间的值为其所包含的的元素的中位数。
现给出n个数的序列a,求将所有长度为奇数的区间的值排序后,第K大的值为多少。
题解:
DP转移时要求一个区间的和,可以用树状数组维护一下。
9.矩阵优化DP
node operator * (node a,node b)
{
for(int i=1;i<=k+2;i++)
for(int j=1;j<=k+2;j++)xu.m[i][j]=0;
for(int i=1;i<=k+2;i++)
for(int j=1;j<=k+2;j++)
for(int p=1;p<=k+2;p++)
xu.m[i][j]=(xu.m[i][j]+a.m[i][p]*b.m[p][j]%mod)%mod;
return xu;
} 10.斜率优化DP
写出方程; 整理式子,将只和j有关的当成y,将i,j都有关的当成kx,将只和i有关的当成b 显然,如果想让F[i]最小,那就是b最小,对于图像上的若干点,选择最下面的一个就是最优的j。
f[0]=0;
int l=1,r=1;
q[1]=0;
for(int i=1;i<=n;i++)
{
while(l<r&&(f[q[l+1]]-f[q[l]])<=(s+sumt[i])*(sumc[q[l+1]]-sumc[q[l]]))l++;
f[i]=f[q[l]]+sumt[i]*(sumc[i]-sumc[q[l]])+s*(sumc[n]-sumc[q[l]]);
while(l<r&&(f[q[r]]-f[q[r-1]])*(sumc[i]-sumc[q[r]])
>=(f[i]-f[q[r]])*(sumc[q[r]]-sumc[q[r-1]]))r--;
q[++r]=i;
} 如果斜率k不单调递增,那么我们就不能随意删点了,查询我们也需要二分找到那个左边斜率比k小,右边比k大的点。
int find(int k)
{
if(l==r)return q[l];
int L=l,R=r;
while(L<R)
{
int mid=(L+R)/2;
if(f[q[mid+1]]-f[q[mid]]<=k*(sumc[q[mid+1]]-sumc[q[mid]]))L=mid+1;
else R=mid;
}
return q[L];
} 如果斜率k和x不单调递增,我们就要用CDQ维护了。
其实也不是CDQ分治,其实就是简单的分治。
#2483. 「CEOI2017」Building Bridges
由于x不单调,就是要动态插入点,平衡树?不存在的。 我们排序就好了,按x排序。 但这样就会出现,原先j更新i,现在j在i的后面。 不过没事,我们可以分治,就是按原序列分治。 因为我们可以用左一半的凸壳来更新右一半。
void solve(int L,int R)
{
if(L==R){Y[L]+=f[L];return;}
int mid=(L+R)/2;
solve(L,mid);
for(int i=L;i<=R;i++)p[i]=i;
sort(p+L,p+R+1,cmp);
l=1,r=0;
for(int i=L;i<=R;i++)
if(p[i]<=mid)
{
while(l<r&&suan(q[r-1],q[r])>=suan(q[r],p[i]))r--;
q[++r]=p[i];
}
for(int i=L;i<=R;i++)
if(p[i]>mid)
{
while(l<r&&suan(q[l],q[l+1])<=2*h[p[i]])l++;
f[p[i]]=min(f[p[i]],f[q[l]]+(h[p[i]]-h[q[l]])*(h[p[i]]-h[q[l]])+w[p[i]-1]-w[q[l]]);
}
solve(mid+1,R);
} 11.决策单调性优化DP
简单来说就是转移方程有函数性。往往和wps二分一起。
有两种解决方法,二分栈和分治。
对于每个,把
看成关于
的函数
。我们要做的就是在所有
的函数中找到最值。
sqrt的增速是递减的,因此可能存在一个j比较小的函数,在某一时刻被j比较大的函数反超。我们大概需要维护这样的若干个函数:
二分栈:
int erfen(int x,int y)
{
int l=y,r=k[x]?k[x]:n,mid,ret=r+1;
while(l<=r)
{
mid=(l+r)/2;
if(suan(mid,x)<=suan(mid,y)){ret=mid,r=mid-1;}
else l=mid+1;
}
return ret;
}
void work()
{
int l=1,r=0;
for(int i=1;i<=n;i++)
{
while(l<r&&suan(k[r-1],q[r])<suan(k[r-1],i))r--;
k[r]=erfen(q[r],i);
q[++r]=i;
while(l<r&&k[l]<=i)l++;
p[i]=max(p[i],suan(i,q[l]));
}
} 分治:
因为f[i]的最优决策点g[i]是单调递增的 solve(l,r,L,R)表示f[l]~f[r]的最优决策点在L~R 令mid=(l+r)/2 O(n)扫一遍取到k[mid] 继续分治 solve(l,mid-1,L,k[mid]) solve (mid+1,r,k[mid],R)
void solve1(int l,int r,int L,int R)
{
if(l>r)return;
int mid=(l+r)/2,g=0;
double tmp=0.0;
f1[mid]=a[mid];
for(int i=L;i<=min(R,mid);i++)
{
tmp=a[i]+sqrt(double(mid-i));
if(tmp>f1[mid])
{
f1[mid]=tmp;
g=i;
}
}
if(g==0)g=mid;
f1[mid]-=a[mid];
solve1(l,mid-1,L,g);
solve1(mid+1,r,g,R);
} 看另一种方程:
往往有函数性。
可以是前缀和,
但
所以就好像是一些斜向上的函数线(增速递增,由于对于一个固定的,
每大1,函数值就大很多)
inline int suan(int x,int y)
{
int l=y+1,r=n,res=n+1;
while(l<=r)
{
int mid=(l+r)/2;
if(dp[x][k-1]+gao(x,mid)>=dp[y][k-1]+gao(y,mid))
{
res=mid;
r=mid-1;
}
else l=mid+1;
}
return res;
}
for(k=1;k<=K;k++)
{
int L=1,R=1;q[L]=0;
for(int i=1,j;i<=n;i++)
{
while(L<R&&suan(q[L],q[L+1])<=i)L++;
j=q[L];
dp[i][k]=dp[j][k-1]+gao(j,i);
while(L<R&&suan(q[R-1],q[R])>=suan(q[R],i))R--;
q[++R]=i;
}
} 12.插头DP(轮廓线DP)
由于要维护连通性,所以不能用0/1来表示一个位置是否有插头,我们要用0(没有),1(起点插头),2(终点插头),来表示。
1.这个点不能填。
2.没有左,上插头,填成右下插头,下是起点,右是终点。
3.有上没左,向下延续或向右拐弯,插头种类不变。
4.有左没上,向右延续或向下拐弯,插头种类不变。
5.左终点上起点,直接相连。
6.左起点上起点,相连并将向右的和上匹配的终点插头改成起点插头。
7.左终点上终点,相连并将向左的和左匹配的起点插头改成终点插头。
8.左起点上终点,只有可能是最后一个格子,判一下即可。
void solve()
{
add(0,1);
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
last=now;
now^=1;
cnt[now]=0;
memset(head,0,sizeof(head));
for(int k=1;k<=cnt[last];k++)
{
node st=unpack(s[k].st[last]);
int num=s[k].num[last];
int zuo=st.s[0],shang=st.s[j];
if(!mp[i][j])
{
if((!zuo)&&(!shang))add(make(st),num);//不填
goto xjh;
}
if((!zuo)&&(!shang))//左和上都没,那么一定填右和下
{
if(mp[i+1][j]&&mp[i][j+1])
{
st.s[0]=2;
st.s[j]=1;
add(make(st),num);
}
goto xjh;
}
if((!zuo)&&shang)//上有左没有
{
if(mp[i+1][j])add(make(st),num);//往下延续
if(mp[i][j+1])//向右转弯
{
st.s[0]=shang;
st.s[j]=0;
add(make(st),num);
}
goto xjh;
}
if(zuo&&(!shang))//左有上没有
{
if(mp[i][j+1])add(make(st),num);//往右延续
if(mp[i+1][j])//向下转弯
{
st.s[0]=0;
st.s[j]=zuo;
add(make(st),num);
}
goto xjh;
}
if(zuo==2&&shang==1)//左终点上起点,直接相连,相当于去掉这两个点
{
st.s[0]=st.s[j]=0;
add(make(st),num);
goto xjh;
}
if(zuo==1&&shang==1)//左起点上起点,找到右边的终点,让它变成起点,去掉这两个点
{
int gs=1,pos;
for(pos=j+1;pos<=m;pos++)
{
if(st.s[pos]==1)gs++;
else if(st.s[pos]==2)gs--;
if(!gs)break;
}
st.s[pos]=1;
st.s[0]=st.s[j]=0;
add(make(st),num);
goto xjh;
}
if(zuo==2&&shang==2)//左终点上终点,找到左边的起点,让它变成终点,去掉这两个点
{
int gs=-1,pos;
for(pos=j-1;pos;pos--)
{
if(st.s[pos]==1)gs++;
else if(st.s[pos]==2)gs--;
if(!gs)break;
}
st.s[pos]=2;
st.s[0]=st.s[j]=0;
add(make(st),num);
goto xjh;
}
if(zuo==1&&shang==2)//只有一种情况成立,即是最后一个要填的格子
{
st.s[0]=st.s[j]=0;
bool flag=0;
for(int pos=1;pos<=m;pos++)
{
if(st.s[pos])
{
flag=1;
break;
}
}
if(!flag&&i==edx&&j==edy)
ans+=num;
}
xjh:;
}
}
}
} 13.动态DP
树剖好理解,每个节点都只维护轻儿子的信息,重儿子可以用树剖维护。
写出状态转移后,把转移搞成矩阵的形式(好转移),每次修改就修改每条重链顶端的父亲。
讲一下流程,每个点记录这个点的轻儿子们的所有F[0/1] 然后树剖维护每个点的值,每个点真实的值就是每个点的 点权+所在重链上的其他点的点权 修改一个点,会对上面log个点的点权发生变化,一个个修改过去即可。 答案就 是根节点所在的重链的权值之和,相当于乘上[0,0]
struct node{
int g[2][2];
}c,val[N],tree[N*4];
node operator + (node a,node b)
{
for(int i=0;i<2;i++)
for(int j=0;j<2;j++)c.g[i][j]=0;
for(int i=0;i<2;i++)
for(int j=0;j<2;j++)
for(int k=0;k<2;k++)c.g[i][j]=max(c.g[i][j],a.g[i][k]+b.g[k][j]);
return c;
}
void dfs(int u,int fat)
{
fa[u]=fat;size[u]=1;f[u][0]=0;f[u][1]=a[u];
for(int i=head[u];i;i=edge[i].next)
{
int v=edge[i].too;
if(v==fat)continue;
dfs(v,u);
size[u]+=size[v];
if(size[v]>size[son[u]])son[u]=v;
f[u][0]+=max(f[v][0],f[v][1]);
f[u][1]+=f[v][0];
}
}
void dfs2(int u,int fat)
{
if(son[u])
{
rev[++seg[0]]=son[u];
seg[son[u]]=seg[0];
top[son[u]]=top[u];
dfs2(son[u],u);
}
for(int i=head[u];i;i=edge[i].next)
{
int v=edge[i].too;
if(!top[v])
{
rev[++seg[0]]=v;
seg[v]=seg[0];
top[v]=v;
dfs2(v,u);
}
}
}
void build(int nod,int l,int r)
{
if(l==r)
{
int u=rev[l],g0=0,g1=a[u];
for(int i=head[u];i;i=edge[i].next)
{
int v=edge[i].too;
if(v==fa[u]||v==son[u])continue;
g0+=max(f[v][0],f[v][1]);
g1+=f[v][0];
}
val[u].g[0][0]=val[u].g[0][1]=g0;
val[u].g[1][0]=g1;
tree[nod]=val[u];
return;
}
int mid=(l+r)/2;
build(nod*2,l,mid);
build(nod*2+1,mid+1,r);
tree[nod]=tree[nod*2]+tree[nod*2+1];
}
node query(int nod,int l,int r,int L,int R)
{
if(l==L&&r==R)return tree[nod];
int mid=(l+r)/2;
if(R<=mid)return query(nod*2,l,mid,L,R);
else if(L>mid)return query(nod*2+1,mid+1,r,L,R);
else return query(nod*2,l,mid,L,mid)+query(nod*2+1,mid+1,r,mid+1,R);
}
node find(int x)
{
return query(1,1,n,L[top[x]],R[top[x]]);
}
void change(int nod,int l,int r,int x)
{
if(l==r)
{
tree[nod]=val[rev[l]];
return;
}
int mid=(l+r)/2;
if(x<=mid)change(nod*2,l,mid,x);
else change(nod*2+1,mid+1,r,x);
tree[nod]=tree[nod*2]+tree[nod*2+1];
}
void update(int x)
{
while(x)
{
node od=find(x);
change(1,1,n,seg[x]);
node nw=find(x);
int u=fa[top[x]];
val[u].g[0][0]=val[u].g[0][0]-max(od.g[0][0],od.g[1][0])+max(nw.g[0][0],nw.g[1][0]);
val[u].g[1][0]=val[u].g[1][0]-od.g[0][0]+nw.g[0][0];
val[u].g[0][1]=val[u].g[0][0];
x=u;
}
}
signed main()
{
n=read();m=read();
for(int i=1;i<=n;i++)a[i]=read();
for(int i=1;i<n;i++)
{
int x=read(),y=read();
add(x,y);
add(y,x);
}
dfs(1,0);
top[1]=rev[1]=seg[0]=seg[1]=1;
dfs2(1,0);
for(int i=1;i<=n;i++)L[top[i]]=n;
for(int i=1;i<=n;i++)
{
L[top[i]]=min(L[top[i]],seg[i]);
R[top[i]]=max(R[top[i]],seg[i]);
}
build(1,1,n);
while(m--)
{
int x=read(),y=read();
val[x].g[1][0]=val[x].g[1][0]-a[x]+y;
a[x]=y;
update(x);
node u=find(1);
printf("%d\n",max(u.g[0][0],u.g[1][0]));
}
return 0;
} 7.数据结构
1.树状数组
树状数组二分:二进制一位一位考虑过去,不需要真的一半。
2.线段树
线段树上二分:先判一边,行就去,不行,去另一边。
权值线段树:下标为权值
动态开点线段树:权值线段树的一种,有些权值(太大)没出现就用动态开点
李超线段树:
对整个横坐标建线段树,每个节点的值存的是一条直线的编号。
要求这条直线在横坐标为mid的时候纵坐标最大。
插入:
如果是叶子节点,我们直接保留最大的。
先比较斜率,再比较横坐标为mid时的纵坐标。
如果新线段的斜率大,并且横坐标为mid时的纵坐标大,那么把原线段下放左区间,节点值改为x
如果新线段的斜率大,但是横坐标为mid时的纵坐标小,那么把新线段下放右区间,不更改节点值
如果新线段的斜率小,但是横坐标为mid时的纵坐标大,那么把原线段下放右区间,节点值改为x
如果新线段的斜率小,并且横坐标为mid时的纵坐标小,那么把新线段下放左区间,不更改节点值
double f(int w,int x)
{
return k[w]*(x-1)+b[w];
}
void change(int nod,int l,int r,int x)
{
if(l==r)
{
if(f(x,l)>f(tree[nod],l))tree[nod]=x;
return;
}
int mid=(l+r)/2;
if(k[tree[nod]]<k[x])
{
if(f(x,mid)>f(tree[nod],mid))
{
change(nod*2,l,mid,tree[nod]);
tree[nod]=x;
}
else change(nod*2+1,mid+1,r,x);
}
if(k[tree[nod]]>k[x])
{
if(f(x,mid)>f(tree[nod],mid))
{
change(nod*2+1,mid+1,r,tree[nod]);
tree[nod]=x ;
}
else change(nod*2,l,mid,x) ;
}
} 查询:
如果是叶子节点直接返回横坐标的值。
如果查询区间在左区间,返回当前的节点的横坐标的值 和 左区间的值 的最大值
如果查询区间在右区间,返回当前的节点的横坐标的值 和 右区间的值 的最大值
double query(int nod,int l,int r,int x)
{
if(l==r)return f(tree[nod],x);
int mid=(l+r)/2;
if(x<=mid)return max(f(tree[nod],x),query(nod*2,l,mid,x));
else return max(f(tree[nod],x),query(nod*2+1,mid+1,r,x));
} P4254 [JSOI2008]Blue Mary开公司(直线)
3.字典树
可持久化字典树:
4.平衡树
普通平衡树:P3369 【模板】普通平衡树
bool chk(int x)
{
return ch[fa[x]][1]==x;
}
void Zhuan(int x)
{
int y=fa[x],z=fa[y],k=chk(x),w=ch[x][k^1];
ch[y][k]=w;fa[w]=y;
ch[z][chk(y)]=x;fa[x]=z;
ch[x][k^1]=y;fa[y]=x;
pushup(y);pushup(x);
}
void splay(int x,int gen=0)
{
while(fa[x]!=gen)
{
int y=fa[x],z=fa[y];
if(z!=gen)
{
if(chk(x)==chk(y))Zhuan(y);
else Zhuan(x);
}
Zhuan(x);
}
if(!gen)root=x;
} 文艺平衡树:就是把下标当成值,注意要多插入0和n+1。维护的值和平衡树的值没有任何关系。
https://www.luogu.org/record/17135871 用文艺平衡树代替线段树。
维护区间加,对于,我们把l-1转成根,把r+1转成根的右儿子,现在的区间
就在根的右儿子的左儿子。
ETT:树,支持将一个节点换一个父亲,子树加,求一个点到根路径的和。
考虑欧拉序,换父亲就是一段括号集体换个位置,我们可以用文艺平衡树来实现。
展示没用自己的写法实现过,先贴别人的吧。
void pushup(int x)
{
sum[x]=sum[ch[x][0]]+sum[ch[x][1]]+val[x];
size[x]=size[ch[x][0]]+size[ch[x][1]]+mark[x];
}
void pushdown(int x){
if(x&&fa[x])pushdown(fa[x]);
if(!x||!lazy[x])return ;
lazy[ch[x][0]]+=lazy[x],lazy[ch[x][1]]+=lazy[x];
val[ch[x][0]]+=lazy[x]*mark[ch[x][0]],val[ch[x][1]]+=lazy[x]*mark[ch[x][1]];
sum[ch[x][0]]+=(long long)size[ch[x][0]]*lazy[x],sum[ch[x][1]]+=(long long)size[ch[x][1]]*lazy[x];
lazy[x]=0;
}
int chk(int x)
{
return ch[fa[x]][1]==x;
}
void Zhuan(int x,int d)
{
int y=ch[x][d^1];
ch[x][d^1]=ch[y][d];
if(ch[x][d^1])fa[ch[x][d^1]]=x;
fa[y]=fa[x];
if(fa[x])ch[fa[x]][chk(x)]=y;
fa[x]=y;ch[y][d]=x;
pushup(x),pushup(y);
}
void splay(int x,int gen=0)
{
pushdown(x);
while(fa[x]!=gen)
{
int y=fa[x],z=fa[y];
if(z!=gen)
{
if(chk(y)==chk(x))Zhuan(z,chk(x)^1);
}
Zhuan(y,chk(x)^1);
}
}
int findl(int x)
{
splay(x);
x=ch[x][0];
while(ch[x][1])x=ch[x][1];
return x;
}
int findr(int x)
{
splay(x);
x=ch[x][1];
while(ch[x][0])x=ch[x][0];
return x;
}
int build(int l,int r)
{
if(l>r)return 0;
int mid=(l+r)/2;
if(l<mid)fa[ch[mid][0]=build(l,mid-1)]=mid;
if(mid<r)fa[ch[mid][1]=build(mid+1,r)]=mid;
val[mid]=(dfn[mid]>0)?a[dfn[mid]]:-a[-dfn[mid]];
mark[mid]=(dfn[mid]>0)?1:-1;
pushup(mid);
return mid;
}
void dfs(int x)
{
dfn[l[x]=++tot]=x;
for(int i=head[x];i;i=e[i].next)
dfs(e[i].to);
dfn[r[x]=++tot]=-x;
} 5.树套树
三维偏序:
矩形修改:
6.kd-tree
个人感觉,kd-tree有两个作用,一个是和树套树一样的矩形修改(空间优秀),一个是求平面点对(感觉就是加剪枝的暴力)。
求平面最第K远/近点对距离。
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define mid (l+r)/2
const int N=1e6+5;
int n,k,cmpd,root;
struct node{
int l,r,d[2],mx[2],mn[2];
}t[N];
struct node2{
int dis;
}tmp;
bool operator < (node2 a,node2 b)
{
return a.dis>b.dis;
}
priority_queue<node2>Q;
int sqr(int x)
{
return x*x;
}
bool cmp(node a,node b)
{
if (a.d[cmpd]!=b.d[cmpd])return a.d[cmpd]<b.d[cmpd];
else return a.d[cmpd^1]<b.d[cmpd^1];
}
void pushup(int x)
{
if(t[x].l)
for(int i=0;i<2;i++)
{
t[x].mx[i]=max(t[x].mx[i],t[t[x].l].mx[i]);
t[x].mn[i]=min(t[x].mn[i],t[t[x].l].mn[i]);
}
if(t[x].r)
for(int i=0;i<2;i++)
{
t[x].mx[i]=max(t[x].mx[i],t[t[x].r].mx[i]);
t[x].mn[i]=min(t[x].mn[i],t[t[x].r].mn[i]);
}
}
int build(int l,int r,int D)
{
cmpd=D;
nth_element(t+l+1,t+mid,t+r+1,cmp);
if(l<mid)t[mid].l=build(l,mid-1,D^1);
if(r>mid)t[mid].r=build(mid+1,r,D^1);
t[mid].mn[0]=t[mid].mx[0]=t[mid].d[0];
t[mid].mn[1]=t[mid].mx[1]=t[mid].d[1];
pushup(mid);
return mid;
}
int dis(int x,int y)
{
return max(sqr((t[x].d[0]-t[y].mn[0])),sqr(t[x].d[0]-t[y].mx[0]))+
max(sqr(t[x].d[1]-t[y].mn[1]),sqr(t[x].d[1]-t[y].mx[1]));
}
int getdis(int x,int y)
{
return sqr(t[x].d[0]-t[y].d[0])+sqr(t[x].d[1]-t[y].d[1]);
}
void query(int u)
{
int dl=0,dr=0,dd=getdis(0,u);
if(dd>Q.top().dis)
{
Q.pop();
tmp.dis=dd;
Q.push(tmp);
}
if(t[u].l)dl=dis(0,t[u].l);
if(t[u].r)dr=dis(0,t[u].r);
tmp=Q.top();
if(dl>dr)
{
if(dl>tmp.dis)query(t[u].l);
tmp=Q.top();
if(dr>tmp.dis)query(t[u].r);
}
else{
if(dr>tmp.dis)query(t[u].r);
tmp=Q.top();
if(dl>tmp.dis)query(t[u].l);
}
}
signed main()
{
scanf("%lld%lld",&n,&k);
for(int i=1;i<=n;i++)scanf("%lld%lld",&t[i].d[0],&t[i].d[1]);
root=build(1,n,0);
tmp.dis=0;
for(int i=1;i<=k*2;i++)Q.push(tmp);
for(int i=1;i<=n;i++)
{
t[0].d[0]=t[i].d[0];
t[0].d[1]=t[i].d[1];
query(root);
}
printf("%lld",Q.top().dis);
return 0;
} 这是矩形修改(我还不会,抄了发孔爷的代码)
bool cmp(int a,int b)
{
return val[a][K]<val[b][K];
}
void pushup(int u)
{
add[u]=0;mul[u]=1;
for(int i=0;i<=1;i++)
{
L[u][i]=min(val[u][i],min(L[ls][i],L[rs][i]));
R[u][i]=max(val[u][i],max(R[ls][i],R[rs][i]));
}
}
void build(int &u,int l,int r,int k)
{
if(l>r)return;
int mid=(l+r)/2;
K=k;
sort(t+l,t+r+1,cmp);
u=t[mid];
build(ls,l,mid-1,k^1);
build(rs,mid+1,r,k^1);
pushup(u);
}
void pushdown(int u)
{
if(mul[u]!=1)
{
(sum[ls]*=mul[u])%=mod;
(sum[rs]*=mul[u])%=mod;
(mul[ls]*=mul[u])%=mod;
(mul[rs]*=mul[u])%=mod;
(add[ls]*=mul[u])%=mod;
(add[rs]*=mul[u])%=mod;
mul[u]=1;
}
if(add[u]!=0)
{
(sum[ls]+=add[u])%=mod;
(sum[rs]+=add[u])%=mod;
(add[ls]+=add[u])%=mod;
(add[rs]+=add[u])%=mod;
add[u]=0;
}
}
bool xu,jia,hao;
void check(int u)
{
xu=jia=hao=true;
for(int i=0;i<=1;i++)
if(x[i]>L[u][i]||R[u][i]>y[i])
{
xu=false;
if(x[i]>val[u][i]||val[u][i]>y[i])
{
jia=false;
if(x[i]>R[u][i]||y[i]<L[u][i])hao=false;
}
}
}
void Add(int u)
{
if(!u)return;
check(u);
if(!hao)return;
if(jia)
{
if(opt==1)(sum[u]+=tmp)%=mod;
else(sum[u]*=tmp)%=mod;
}
if(xu)
{
if(opt==1)(add[u]+=tmp)%=mod;
else{
(add[u]*=tmp)%=mod;
(mul[u]*=tmp)%=mod;
}
return ;
}
pushdown(u);
Add(ls);
Add(rs);
}
void find(int u,int v)
{
if(!u)return;
if(val[v][0]<L[u][0]||val[v][0]>R[u][0]||val[v][1]<L[u][1]||val[v][1]>R[u][1])return;//不在这个区间
if(u==v)
{
printf("%lld\n",sum[u]);
return ;
}
pushdown(u);
find(ls,v);
find(rs,v);
} 8.线性做法
1.单调队列
2.尺取法
3.单调栈
求最大全0/1矩阵面积
我们维护每个点向左,向右,向上最多能扩展到哪。
求面积的时候,我们只有在上面顶住的时候才算面积(很好理解的)
for(int i=1;i<=n;i++)
for(int j=2;j<=m;j++)
if(a[i][j]==1&&a[i][j-1]==1)l[i][j]=l[i][j-1];
for(int i=1;i<=n;i++)
for(int j=m-1;j>0;j--)
if(a[i][j]==1&&a[i][j+1]==1)r[i][j]=r[i][j+1];
for(int i=2;i<=n;i++)
for(int j=1;j<=m;j++)
if(a[i][j]==1&&a[i-1][j]==1)
{
h[i][j]=h[i-1][j]+1;
l[i][j]=max(l[i][j],l[i-1][j]);
r[i][j]=min(r[i][j],r[i-1][j]);
}
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
if(a[i][j]==1)
ans=max(ans,h[i][j]*(r[i][j]-l[i][j]+1)); 根据障碍格来求的最大矩阵
维护每个点向右的矩阵,当然向左也要。
然后还有一种特殊情况就是,按y排序后相邻两个的矩形。
for(int i=1;i<=n;i++)a[i]=(node){read(),read()};
a[++n]=(node){0,0};
a[++n]=(node){0,w};
a[++n]=(node){l,0};
a[++n]=(node){l,w};
sort(a+1,a+n+1,cmp1);
for(int i=1;i<=n;i++)
{
int U=w,D=0,L=a[i].x;
for(int j=i+1;j<=n;j++)
{
ans=max(ans,(a[j].x-L)*(U-D));
if(a[j].y>=a[i].y)U=min(U,a[j].y);
else D=max(D,a[j].y);
}
}
for(int i=n;i;i--)
{
int U=w,D=0,R=a[i].x;
for(int j=i-1;j;j--)
{
ans=max(ans,(R-a[j].x)*(U-D));
if(a[j].y>=a[i].y)U=min(U,a[j].y);
else D=max(D,a[j].y);
}
}
sort(a+1,a+n+1,cmp2);
for(int i=1;i<n;i++)
ans=max(ans,l*(a[i+1].y-a[i].y)); 4.长链剖分
用于维护长度的题目。比如:求一个最大的连通块大小,联通块直径不超过
题目:
一颗个点的树,要给每个点染一个颜色,要求任意两个距离小于等于
的点颜色不同,问最少的颜色数。
众所周知 树上距离小于等于k的点连成的图是一张弦图(所以可以直接用完美消除序列(bfs序)+点分树)
众所周知 弦图最小染色数等于最大团
于是问题变成,求一个连通块,直径不超过。
表示
的子树内,强制选
,深度为
的最大值
根据套路,考虑长链剖分。
我们把取前缀max,维护深度不超过
的最大值。
因为取过前缀max,所以在可行的情况下,另一边长度越长越好,
也就是最长的位置要么就是卡到长度为,要么就是卡到那条链的底端,要么就是不能超过当前更新的长度
记的最长深度为
,三种情况分类:
1.,
可能会小于
2.且
,那么转移肯定从
处转
3.且
,那么转移肯定从
处转
发现中间部分相当于一个区间加,只需要打一个全局的即可
#include<bits/stdc++.h>
using namespace std;
#define next Next
#define gc getchar
#define int long long
const int N=1e6+5;
int n,m,ans,son[N],dis[N],tag[N];
int dp[N],*id=dp,*f[N];
vector<int>g[N];
//char buf[1<<21],*p1=buf,*p2=buf;
//inline int gc(){return p1==p2&&(p2=(p1=buf)+fread(buf,1,1<<21,stdin),p1==p2)?EOF:*p1++;}
inline int read()
{
int ret=0,f=0;char c=gc();
while(!isdigit(c)){if(c=='-')f=1;c=gc();}
while(isdigit(c)){ret=ret*10+c-48;c=gc();}
if(f)return-ret;return ret;
}
void dfs(int u,int fa)
{
for(int i=0;i<g[u].size();i++)
{
int v=g[u][i];
if(v==fa)continue;
dfs(v,u);
if(dis[v]+1>dis[u])
{
dis[u]=dis[v]+1;
son[u]=v;
}
}
}
void solve(int u,int fa)
{
f[u]=id++;
if(son[u])
{
solve(son[u],u);
tag[u]=tag[son[u]];
}
f[u][0]=-tag[u];
tag[u]++;//多了点u
int len=min(dis[u],m);
for(int j=0;j<g[u].size();j++)
{
int v=g[u][j];
if(v==fa||v==son[u])continue;
solve(v,u);
int d=min(dis[v],m);
for(int i=0;i<=d;i++)
f[v][i]+=tag[v];
tag[v]=0;
for(int i=d;i>=0;i--)//注意k<=i
{
int k=min(i,m-i);
int x=f[u][i]+(k>0?f[v][k-1]:0);
int y=f[u][k]+(i>0?f[v][i-1]:0);
f[u][i]=max(x,y);
}
//x<=d,x可能会小于y+1
int lim=max(d,m-d-1);
tag[u]+=f[v][d];
//x>d且x+d<=m,那么转移肯定从d处转
for(int i=0;i<=d;i++)f[u][i]-=f[v][d];
for(int i=lim+1;i<=len;i++)f[u][i]-=f[v][d];
for(int i=lim+1;i<=len;i++)
if(m-i-1>=0)f[u][i]+=f[v][m-i-1];
//x>d且x+d>m,那么转移肯定从m-x-1处转
for(int i=1;i<=d;i++)f[u][i]=max(f[u][i-1],f[u][i]);
for(int i=lim+1;i<=len;i++)f[u][i]=max(f[u][i-1],f[u][i]);
//为啥[d+1,m-d-1]不用取max呢
//因为都是加了f[v][d],还是递增的
}
ans=max(ans,f[u][len]+tag[u]);
}
signed main()
{
freopen("rs.in","r",stdin);
freopen("rs.out","w",stdout);
n=read();m=read();
for(int i=1;i<n;i++)
{
int x=read(),y=read();
g[x].push_back(y);
g[y].push_back(x);
}
dfs(1,0);
solve(1,0);
cout<<ans;
return 0;
}
5.单调性
6.并查集
9.字符串
1.hash
2.kmp
求最长的前缀等于后缀。
void pre()
{
int j=0;
p[1]=0;
for(int i=1;i<m;i++)
{
while(j>0&&b[j+1]!=b[i+1]) j=p[j];
if(b[j+1]==b[i+1]) j++;
p[i+1]=j;
}
}
void kmp()
{
int j=0;
for(int i=0;i<n;i++)
{
while(j>0&&b[j+1]!=a[i+1]) j=p[j];
if(b[j+1]==a[i+1]) j++;
if(j==m)
{
printf("%d\n",i+1-m+1);
j=p[j];
}
}
} 3.ac自动机
多模式串匹配
void make(string s,int k)
{
int u=1,len=s.size();
for(int i=0;i<len;i++)
{
int c=s[i]-'a';
if(!ch[u][c])
{
ch[u][c]=++cnt;
memset(ch[cnt],0,sizeof(ch[cnt]));
}
u=ch[u][c];
}
bo[u]=k;
}
void bfs()
{
int l=1,r=1;
q[1]=1;nxt[1]=0;
while(l<=r)
{
int u=q[l++];
for(int i=0;i<26;i++)
{
if(!ch[u][i])ch[u][i]=ch[nxt[u]][i];
else{
q[++r]=ch[u][i];
nxt[ch[u][i]]=ch[nxt[u]][i];
}
}
}
}
void find(string s)
{
int u=1,len=s.size(),c,k;
for(int i=0;i<len;i++)
{
c=s[i]-'a';
k=ch[u][c];
while(k>1)
{
ans[bo[k]]++;
k=nxt[k];
}
u=ch[u][c];
}
} 有重复模式串的多重匹配 。
每次跳nxt,然后把结尾的所有串加一,妥妥的T。
转化一下建出一颗nxt树,链加,子树求和,直接树上差分。
void dfs(int u)
{
for(int i=0;i<g[u].size();i++)
{
int v=g[u][i];
dfs(v);
size[u]+=size[v];
}
}
signed main()
{
n=read();
for(int i=1;i<=n;i++)
{
scanf("%s",s);
int m=strlen(s);
int u=1;
for(int j=0;j<m;j++)
{
int c=s[j]-'a';
if(!ch[u][c])ch[u][c]=++cnt;
u=ch[u][c];
}
match[i]=u;
}
for(int i=0;i<26;i++)ch[0][i]=1;
queue<int>q;
q.push(1);
while(!q.empty())
{
int u=q.front();q.pop();
for(int i=0;i<26;i++)
{
if(ch[u][i])
{
nxt[ch[u][i]]=ch[nxt[u]][i];
q.push(ch[u][i]);
}
else ch[u][i]=ch[nxt[u]][i];
}
}
scanf("%s",s);
int m=strlen(s);
int u=1;
for(int i=0;i<m;i++)
{
u=ch[u][s[i]-'a'];
size[u]++;
}
for(int i=2;i<=cnt;i++)g[nxt[i]].push_back(i);
dfs(1);
for(int i=1;i<=n;i++)printf("%d\n",size[match[i]]);
return 0;
} 4.sam(sa)
个人感觉sam虽然裸,但不好学,不是有深入了解的话建议别写。
sa就比较好懂了(扩展),往往要和线段树或主席树一起用。
void SA(int n,int m)
{
int p=0;
for(int i=1;i<=m;i++)c[i]=0;
for(int i=1;i<=n;i++)c[x[i]=a[i]]++;
for(int i=1;i<=m;i++)c[i]+=c[i-1];
for(int i=n;i;i--)sa[c[x[i]]--]=i;
for(int i=1;i;i=i*2)
{
p=0;
for(int j=n-i+1;j<=n;j++)y[++p]=j;
for(int j=1;j<=n;j++)
if(sa[j]>i)y[++p]=sa[j]-i;
for(int j=1;j<=m;j++)c[j]=0;
for(int j=1;j<=n;j++)c[x[y[j]]]++;
for(int j=1;j<=m;j++)c[j]+=c[j-1];
for(int j=n;j;j--)sa[c[x[y[j]]]--]=y[j];
swap(x,y);
x[sa[1]]=1;
p=2;
for(int j=2;j<=n;j++)
if(y[sa[j]]==y[sa[j-1]]&&y[sa[j]+i]==y[sa[j-1]+i])x[sa[j]]=p-1;
else x[sa[j]]=p++;
if(p>n)return;
m=p;
}
}
void Height()
{
int k=0;
for(int i=1;i<=n;i++)rk[sa[i]]=i;
for(int i=1;i<=n;i++)
{
if(rk[i]==1)continue;
if(k)k--;
int j=sa[rk[i]-1];
while(i+k<=n&&j+k<=n&&a[i+k]==a[j+k])k++;
height[rk[i]]=k;
}
}
void ST()
{
memset(st,0x3f,sizeof(st));
for(int i=1;i<=n;i++)st[i][0]=height[i];
for(int i=1;i<=18;i++)
for(int j=1;j<=n;j++)
st[j][i]=min(st[j][i-1],st[j+(1<<(i-1))][i-1]);
}
int query(int l,int r)
{
int L=l,R=r;
l=min(rk[L],rk[R])+1;
r=max(rk[L],rk[R]);
int k=Log[r-l+1];
return min(st[l][k],st[r-(1<<k)+1][k]);
}
signed main()
{
scanf("%s",a+1);
n=strlen(a+1);
Log[0]=-1;
for(int i=1;i<=n;i++)Log[i]=Log[i/2]+1;
SA(n,233);
Height();
ST();
m=read();
while(m--)
{
int l=read(),r=read();
printf("%d\n",query(l,r));
}
return 0;
} 10.数学
1.质数
2.gcd
关于的不定方程
有整数解的充要条件是
scanf("%d",&n);
int ans;
scanf("%d",&ans);
for(int i=2;i<=n;i++)
{
scanf("%d",&x);
ans=__gcd(ans,x);
}
cout<<abs(ans); 3.exgcd
从可以推出
由于都可以除
,所以最小的
就是
,最小
同理
void exgcd(int a,int b,int &d,int &x,int &y)
{
if(b==0){d=a;x=1;y=0;}
else{exgcd(b,a%b,d,y,x);y-=x*(a/b);}
} 4.bsgs
把x换一下,
移项得:
取
int kuai(int a,int b)
{
int res=1;
while(b)
{
if(b%2==1)res=res*a%mod;
a=a*a%mod;
b=b/2;
}
return res;
}
int bsgs(int a,int b,int p)
{
a%=p;b%=p;
if(!a&&!b)return 1;
if(!a||!b)return 0;
int m=ceil(sqrt(p)),tmp=1;
mp[b]=0;
for(int i=1;i<=m;i++)
{
tmp=tmp*a%p;
if(!mp[tmp*b%p])mp[tmp*b%p]=i;
}
int t=1;
for(int i=1;i<=m;i++)
{
t=t*tmp%p;
if(mp[t])
{
int ans=i*m-mp[t];
return(ans%p+p)%p;
}
}
} 5.crt
参考题解:中国剩余定理 && 扩展中国剩余定理
注意:快速乘
假设已经求出前k-1个方程组成的同余方程组的一个解为x。
且有
则前k-1个方程的通解为
那么加入第k个方程后,我们就是要求一个正整数t,使得
转化一下式子
对于这个式子我们就可以通过扩欧求解t
若该同余式无解,则整个方程组无解
若有,则前k个同余式组成的方程组的一个解为
int kuai(int a,int b,int mod)
{
if(b==0)return 0;
if(b==1)return a;
int x=kuai(a,b/2,mod);
if(b%2==0)return(x+x)%mod;
else return(x+x+a)%mod;
}
void exgcd(int a,int b,int &d,int &x,int &y)
{
if(b==0){d=a;x=1;y=0;}
else{exgcd(b,a%b,d,y,x);y-=x*(a/b);}
}
int excrt()
{
int M=m[1],ans=a[1];
for(int i=2;i<=n;i++)
{
int A=M,B=m[i],C=a[i]-ans;
exgcd(A,B,d,X,Y);
ans+=M*kuai(X,(C%B+B)%B/d,B/d);
M=M/d*m[i];
ans=(ans%M+M)%M;
}
return ans;
} 6.lucas
注意:
int kuai(int a,int b)
{
int res=1;
while(b)
{
if(b%2==1)res=res*a%mod;
a=a*a%mod;
b=b/2;
}
return res;
}
int C(int n,int m)
{
if(m>n)return 0;
return jc[n]*inv[m]%mod*inv[n-m]%mod;
}
int lucas(int n,int m)
{
if(m==0)return 1;
return C(n%mod,m%mod)*lucas(n/mod,m/mod)%mod;
} 7.二次剩余
求: 其中p是一个奇质数。
如果是p=2的话就把0,1带入求解即可。
有二次剩余的条件:
题目:方程的解
while(T--)
{
a=read(),b=read(),p=read();
if(p<=2)
{
if(b%p==0||(a+b+1)%p==0)puts("Yes");
else puts("No");
continue;
}
if(a&1)a+=p;
n=((a*a/4%p-b)%p+p)%p;
if(n==0||kuai(n,(p-1)/2)==1)puts("Yes");
else puts("No");
} 11.分治
1.普通分治(求方案数)
void solve(int l,int r)
{
if(l==r)return;
int mid=(l+r)/2;
solve(l,mid);//左区间自己
solve(mid+1,r);//右区间自己
for(int i=l;i<=mid;i++)//枚举左边
//往往是双指针或数据结构,时间复杂度1/2只log
} 区间异或和异或区间最大值异或区间最小值 (分治+双指针(线段树上二分)+可持久化trie)
void solve(int l,int r)
{
if(l>r)return;
if(l==r)
{
ans=max(a[l],ans);
return;
}
int mid=(l+r)>>1;
int suml=0,maxl=-INF,minl=INF,maxr=-INF,minr=INF,sumr=0;
int pin1,pin2;
for(int i=mid;i>=l;i--)
{
suml^=a[i];
maxl=max(maxl,a[i]);
minl=min(minl,a[i]);
sumarray[i]=suml;
minarray[i]=minl;
maxarray[i]=maxl;
}
for(int i=mid+1;i<=r;i++)
{
sumr^=a[i];
maxr=max(maxr,a[i]);
minr=min(minr,a[i]);
sumarray[i]=sumr;
minarray[i]=minr;
maxarray[i]=maxr;
}
//1.max left min left
trie.clear();
root[mid]=0;
for(int i=mid+1;i<=r;i++)
trie.insert(root[i],root[i-1],sumarray[i],30);
pin1=mid+1;
for(int i=mid;i>=l;i--)
{
while(pin1<=r&&minarray[pin1]>=minarray[i]&&maxarray[pin1]<=maxarray[i])pin1++;
//¸æ·Ç£¬Ã»Ïëµ½£¬Ð´ÁËÏß¶ÎÊ÷É϶þ·Ö£¬×Ô±Õ
int temp=minarray[i]^maxarray[i]^sumarray[i];
ans=max(ans,temp);
if(pin1>mid+1)ans=max(ans,trie.que(root[mid],root[pin1-1],temp));
}
//2.max right min right
trie.clear();
root[mid+1]=0;
for(int i=mid;i>=l;i--)
trie.insert(root[i],root[i+1],sumarray[i],30);
pin1=mid;
for(int i=mid+1;i<=r;i++)
{
while(pin1>=l&&minarray[pin1]>=minarray[i]&&maxarray[pin1]<=maxarray[i])pin1--;
int temp=minarray[i]^maxarray[i]^sumarray[i];
ans=max(ans,temp);
if(pin1<mid)ans=max(ans,trie.que(root[mid+1],root[pin1+1],temp));
}
//3.max left min right
trie.clear();
root[mid]=0;
for(int i=mid+1;i<=r;i++)
trie.insert(root[i],root[i-1],sumarray[i]^minarray[i],30);
pin1=pin2=mid+1;
for(int i=mid;i>=l;i--)
{
while(pin1<=r&&minarray[i]<minarray[pin1])++pin1;
while(pin2<=r&&maxarray[i]>=maxarray[pin2])++pin2;
if(pin1<pin2)
{
int temp=sumarray[i]^maxarray[i];
ans=max(ans,trie.que(root[pin1-1],root[pin2-1],temp));
}
}
//4.max right min left
trie.clear();
root[mid+1]=0;
for(int i=mid;i>=l;i--)
trie.insert(root[i],root[i+1],sumarray[i]^minarray[i],30);
pin1=pin2=mid;
for(int i=mid+1;i<=r;i++)
{
while(pin1>=l&&minarray[i]<minarray[pin1])--pin1;
while(pin2>=l&&maxarray[i]>=maxarray[pin2])--pin2;
if(pin1>pin2)
{
int temp=sumarray[i]^maxarray[i];
ans=max(ans,trie.que(root[pin1+1],root[pin2+1],temp));
}
}
solve(l,mid);
solve(mid+1,r);
return;
} 2.CDQ分治
三维偏序问题。【模板】三维偏序(陌上花开) 数据结构板子题
void solve(int l,int r)//CDQ分治的思想就是算左边对右边的贡献
{
if(l==r)return;
int mid=(l+r)/2;
solve(l,mid);solve(mid+1,r);
int L=l,R=mid+1,tot=l;
while(L<=mid&&R<=r)
{
if(p[L].b<=p[R].b)//将第二维小于的都放到树状数组中
{
add(p[L].c,1);
Q[tot++]=p[L++];
}
else{
Ans[p[R].id]=find(p[R].c);
Q[tot++]=p[R++];
}
}
while(L<=mid)
{
add(p[L].c,1);
Q[tot++]=p[L++];
}
while(R<=r)
{
Ans[p[R].id]=find(p[R].c);
Q[tot++]=p[R++];
}
for(int i=l;i<=mid;i++)add(p[i].c,-1);
for(int i=l;i<=r;i++)p[i]=Q[i];//把第二维也排好序
}
sort(p+1,p+n+1);//排第一维
solve(1,n);//CDQ分治 3.分治FFT
void CDQ_FFT(int *F,int *G,int l,int r)
{
if(l==r)return;
int mid=(l+r)/2;
CDQ_FFT(F,G,l,mid);
for(int i=l;i<=mid;i++)A[i-l]=F[i];
for(int i=0;i<=r-l;i++)B[i]=G[i];
int len=1,bit=0;
while(len<=r-l+1)
{
len=len*2;
bit++;
}
bit--;
work(len,bit);
for(int i=mid-l+1;i<=len;i++)A[i]=0;
for(int i=r-l+1;i<=len;i++)B[i]=0;
ntt(A,len,1);
ntt(B,len,1);
for(int i=0;i<len;i++)A[i]=1ll*A[i]*B[i]%mod;
ntt(A,len,-1);
for(int i=mid+1;i<=r;i++)F[i]=(F[i]+A[i-l])%mod;
CDQ_FFT(F,G,mid+1,r);
} 式子推导就不写了(我也不会)
这部分是每次在
的时候加上就好。
对于后面的自己卷自己,我们需要特别处理。
板子,我们是考虑
对
的贡献。
贡献就是。
如今自己卷自己,发现如果当时,原先的卷法完全可以(因为
)
但当时,由于
还没算出,无法卷。
解决方案:
当做已经算出,先卷进去。然后对于每一个
的区间,由于如今是
都算出了,我们在把之前没算的贡献加回去。
之前没算的贡献就是和
中各任选一个的和(仔细想想为啥是
呢?,注意:递归),
和
的总贡献是
和
,两项加起来是
。把这两段做一个卷积即可。
注意:DP转移式子中是从
开始的,前面写的一些
,其实是
。
void CDQ(int l,int r)
{
if(l==r)
{
f[l]=add(f[l],mul(f[l-1],l-1));
return;
}
int mid=(l+r)/2;
CDQ(l,mid);
if(l>2)
{
int lp=2,rp=min(l-1,r-l),len=1;
while(len<=rp+mid-l)len=len*2;
fill(A,A+len,0);
fill(B,B+len,0);
for(int i=lp;i<=rp;i++)A[i]=f[i];
for(int i=l;i<=mid;i++)B[i-l]=f[i];
NTT(A,len,1);NTT(B,len,1);
for(int i=0;i<len;i++)C[i]=mul(A[i],B[i]);
NTT(C,len,-1);
for(int i=mid+1;i<=r;i++)
f[i]=add(f[i],mul(C[i-l],i-2));
}
int len=1;
while(len<=mid-l+mid-l)len=len*2;
fill(A,A+len,0);
fill(B,B+len,0);
for(int i=l;i<=mid;i++)A[i-l]=f[i];
for(int i=l;i<=mid;i++)B[i-l]=mul(i-1,f[i]);
NTT(A,len,1);NTT(B,len,1);
for(int i=0;i<len;i++)C[i]=mul(A[i],B[i]);
NTT(C,len,-1);
for(int i=mid+1;i<=r;i++)
f[i]=add(f[i],C[i-l-l]);
CDQ(mid+1,r);
} 4.整体二分
整体二分可以和很多东西配,比如并查集。
int find(int x)
{
if(fa[x]==x)return x;
return find(fa[x]);
}
void merge(int x,int y)
{
x=find(x),y=find(y);
if(x==y)return;
if(size[x]>size[y])swap(x,y);
zhan[++tot]=x;
fa[x]=y;
size[y]+=size[x];
}
void Delete()
{
while(tot)
{
int x=zhan[tot];
size[fa[x]]-=size[x];
fa[x]=x;
tot--;
}
}
void solve(int l,int r,int L,int R)
{
// cout<<l<<" "<<r<<" "<<L<<" "<<R<<endl;
if(L==R)
{
for(int i=l;i<=r;i++)Ans[Q[i].id]=L;
merge(e[L].x,e[L].y);
return;
}
int mid=(L+R)/2;
int LL=l-1,RR=r+1;
tot=0;
for(int i=L;i<=mid;i++)
merge(e[i].x,e[i].y);
for(int i=l;i<=r;i++)
{
int x=find(Q[i].x),y=find(Q[i].y);
if(x==y)
{
if(size[x]>=Q[i].z)xu[++LL]=Q[i];
else xu[--RR]=Q[i];
}
else{
if(size[x]+size[y]>=Q[i].z)xu[++LL]=Q[i];
else xu[--RR]=Q[i];
}
}
for(int i=l;i<=r;i++)Q[i]=xu[i];
Delete();
solve(l,LL,L,mid);
solve(RR,r,mid+1,R);
} 12.矩阵
1.矩阵乘法
2.矩阵乘法优化DP
3.循环矩阵
初始矩阵就是原先矩阵的第一列。
node operator * (node a,node b)
{
for(int i=0;i<k;i++)c.m[i]=0;
for(int i=0;i<k;i++)
for(int j=0;j<k;j++)
c.m[(i+j)%k]=(c.m[(i+j)%k]+a.m[i]*b.m[j])%mod;
return c;
}
node kuai(node a,int b)
{
node res;
for(int i=0;i<k;i++)res.m[i]=0;
res.m[0]=1;
while(b)
{
if(b&1)res=res*a;
b=b/2;
a=a*a;
}
return res;
} 题目:
有一个长度为 的环,执行
次操作,在一次操作中,
对于每一个数,它变为它左边的数乘上 以及它本身以及它右边的数乘上
的和。
求最后每一个位置上的数是多少。(计算时左边和右边的数都是上一次的数)
13.点分治
1.统计边
2.点分树
点分树有一个性质:点分树上的两点的LCA,在原树两点的路径上。
所以我们把x-y的路径分成x-点分树上的LCA和点分树上的LCA-y的两条路径。
3.动态点分治
点分树上树高log,由于是二叉树,那么只要和点u在不同儿子那么就能计算答案。
每个节点都维护向下能到哪些点和距离,然后就可以二分了。
void dfs1(int u,int fa)//求出每个点的size
{
size[u]=1;
for(int i=head[u];i;i=edge[i].next)
{
int v=edge[i].too;
if(v==fa)continue;
if(vis[v])continue;
dfs1(v,u);
size[u]+=size[v];
}
}
void dfs2(int u,int fa)//找到重心
{
int mx=sum-size[u];
for(int i=head[u];i;i=edge[i].next)
{
int v=edge[i].too;
if(v==fa)continue;
if(vis[v])continue;
dfs2(v,u);
mx=max(mx,size[v]);
}
if(mx<ma){ma=mx,rt=u;}
}
void dfs3(int u,int fat,int g,int t)//维护fa和ans
{
fa[u].push_back((node){g,dep[u],t});
ans[g][t].push_back((Node){a[u],1,dep[u]});
for(int i=head[u];i;i=edge[i].next)
{
int v=edge[i].too;
if(v==fat)continue;
if(vis[v])continue;
dep[v]=dep[u]+edge[i].zh;
dfs3(v,u,g,t);
}
}
void solve(int u)
{
dfs1(u,0);
if(size[u]==1)
{
vis[u]=1;
fa[u].push_back((node){u,0,-1});
return;
}
sum=size[u];
ma=0x3f3f3f3f;
dfs2(u,0);
vis[rt]=1;
fa[rt].push_back((node){rt,0,-1});
for(int i=head[rt],t=0;i;i=edge[i].next)
{
int v=edge[i].too;
if(vis[v])continue;
dep[v]=edge[i].zh;
dfs3(v,0,rt,t);
ans[rt][t].push_back((Node){0x3f3f3f3f,0,0});
sort(ans[rt][t].begin(),ans[rt][t].end());
for(int j=ans[rt][t].size()-2;j>=0;j--)
{
ans[rt][t][j].gs+=ans[rt][t][j+1].gs;
ans[rt][t][j].dis+=ans[rt][t][j+1].dis;
}
t++;
}
for(int i=head[rt];i;i=edge[i].next)
{
int v=edge[i].too;
if(vis[v])continue;
solve(v);
}
}
void query(int l,int r,int u)
{
res=0;
vector<Node>::iterator L,R;
for(int i=0;i<fa[u].size();i++)
{
int f=fa[u][i].f;
for(int j=0;j<=2;j++)//由于每个点的度<=3
{
if(j==fa[u][i].id||ans[f][j].empty())continue;
L=lower_bound(ans[f][j].begin(),ans[f][j].end(),(Node){l,0,0});
R=upper_bound(ans[f][j].begin(),ans[f][j].end(),(Node){r,0,0});
res+=fa[u][i].dis*(L->gs-R->gs)+L->dis-R->dis;
}
if(l<=a[f]&&a[f]<=r)res+=fa[u][i].dis;
}
} 动态修改点权,求带权重心。
void Addedge(int u,int v,int w)
{
G[++tot].u=u;G[tot].v=v;G[tot].w=w;G[tot].next=head[u];head[u]=tot;
}
struct node{
int head[N],cnt,tot;
int st[N<<2][21],dis[N],id[N<<1];
struct edge{int u,v,next,w;}E[2*N];
inline void addedge(int u,int v,int w)
{
E[++tot].u=u;E[tot].v=v;E[tot].w=w;E[tot].next=head[u];head[u]=tot;
E[++tot].u=v;E[tot].v=u;E[tot].w=w;E[tot].next=head[v];head[v]=tot;
}
int getdis(int u,int v)
{
if(id[u]>id[v])swap(u,v);
int k=Log[id[v]-id[u]+1];
return dis[u]+dis[v]-2*min(st[id[u]][k],st[id[v]-(1ll<<k)+1][k]);
}
void dfs(int u,int fa)
{
st[++cnt][0]=dis[u];
id[u]=cnt;
for(int i=head[u];i;i=E[i].next)
{
int v=E[i].v;
if(v==fa)continue;
dis[v]=dis[u]+E[i].w;
dfs(v,u);
st[++cnt][0]=dis[u];
}
}
void initrmq()
{
memset(id,0,sizeof(id));
cnt=0;tot=0;dis[1]=0;
dfs(1,0);
for(int j=1;(1ll<<j)<=cnt;j++)
for(int i=1;i+(1ll<<j)-1<=cnt&&i<=cnt;i++)
st[i][j]=min(st[i][j-1],st[i+(1ll<<(j-1))][j-1]);
}
}T;
void getroot(int u,int fa)
{
size[u]=1;
f[u]=0;
for(int i=T.head[u];i;i=T.E[i].next)
{
int v=T.E[i].v;
if(vis[v]||v==fa)continue;
getroot(v,u);
size[u]+=size[v];
f[u]=max(f[u],size[v]);
}
f[u]=max(f[u],sum-size[u]);
if(f[u]<f[rt])rt=u;
}
void work(int u,int fa)
{
vis[u]=1;par[u]=fa;
for(int i=T.head[u];i;i=T.E[i].next)
{
int v=T.E[i].v;
if(vis[v])continue;
sum=size[v];
f[0]=size[v];
rt=0;
getroot(v,0);
Addedge(u,rt,v);
work(rt,u);
}
}
void change(int u,int val)
{
sumv[u]+=val;
for(int i=u;par[i];i=par[i])
{
int dist=T.getdis(par[i],u);
dis1[par[i]]+=dist*val;
dis2[i]+=dist*val;
sumv[par[i]]+=val;
}
}
int calc(int u)
{
int ans=dis1[u];
for(int i=u;par[i];i=par[i])
{
int dist=T.getdis(par[i],u);
ans+=dis1[par[i]]-dis2[i];
ans+=dist*(sumv[par[i]]-sumv[i]);
}
return ans;
}
int query(int u)
{
int ans=calc(u);
for(int i=head[u];i;i=G[i].next)
{
int tmp=calc(G[i].w);
if(tmp<ans)return query(G[i].v);
}
return ans;
} P6329 【模板】点分树 | 震波
点分树最简单的写法就是维护两个数据结构(一般是vector)
一个记录点分树上以点u为根的子树与u的信息(到u的距离)
一个记录点分树上以点u为根的子树与fa[u]的信息(到法fa[u]的距离)
然后就可以通过容斥(减一减),求出在fa[u]的子树中且不在u的子树中的信息(到某某点的距离)
除了这种写法,就要重构树(使度数<=3)了
这题还有一个难点就是这两个数据结构是树状数组
那么就要动态开点(vector),有点麻烦
距离为0的点(本身),树状数组不好处理,我们就先不加入,到要用的时候直接加
#include<bits/stdc++.h>
using namespace std;
#define next Next
//#define gc getchar
//#define int long long
const int N=2e6+5;
int n,m,top,t,sum,rt,lastans,sz[N],fa[N],vis[N],ru[N],id[N],dep[N],a[N];
int head[N],ma[N],Log[N],size[N],st[N][21];
struct node{
int too,next;
}edge[N*2];
char buf[1<<21],*p1=buf,*p2=buf;
inline int gc(){return p1==p2&&(p2=(p1=buf)+fread(buf,1,1<<21,stdin),p1==p2)?EOF:*p1++;}
inline int read()
{
int ret=0,f=0;char c=gc();
while(!isdigit(c)){if(c=='-')f=1;c=gc();}
while(isdigit(c)){ret=ret*10+c-48;c=gc();}
if(f)return -ret;return ret;
}
void write(int x)
{
if(x<10){putchar(x+'0');return;}
write(x/10);
putchar(x%10+'0');
}
int lowbit(int x)
{
return x&-x;
}
struct Tree{
vector<int>c;
void init(int n)
{
for(int i=0;i<=n;i++)
c.push_back(0);
}
void change(int x,int y)
{
int n=c.size()-1;
while(x<=n)
{
c[x]+=y;
x+=lowbit(x);
}
}
int find(int x)
{
int res=0;
if(x>c.size()-1)x=c.size()-1;//注意
while(x>0)
{
res+=c[x];
x-=lowbit(x);
}
return res;
}
}T1[N],T2[N];
void add(int a,int b)
{
edge[++top].too=b;edge[top].next=head[a];head[a]=top;
}
void dfs(int u,int fa,int d)
{
ru[u]=++t;
id[t]=u;
dep[t]=d;
for(int i=head[u];i;i=edge[i].next)
{
int v=edge[i].too;
if(v==fa)continue;
dfs(v,u,d+1);
id[++t]=u;
dep[t]=d;
}
}
int LCA(int x,int y)
{
int l=ru[x],r=ru[y];
if(l>r)swap(l,r);
int k=Log[r-l+1];
x=st[l][k];y=st[r-(1<<k)+1][k];
if(dep[x]<=dep[y])return id[x];
return id[y];
//注意LCA是要转化为id的
}
int dis(int x,int y)
{
int lca=LCA(x,y);
return dep[ru[x]]+dep[ru[y]]-2*dep[ru[lca]];
//注意这里的dep[x]是编号为x的点的深度,不是点x的深度
}
void getrt(int u,int fa)
{
ma[u]=0;size[u]=1;
for(int i=head[u];i;i=edge[i].next)
{
int v=edge[i].too;
if(v==fa||vis[v])continue;
getrt(v,u);
size[u]+=size[v];
ma[u]=max(ma[u],size[v]);
}
ma[u]=max(ma[u],sum-size[u]);
if(ma[u]<ma[rt])rt=u;
}
void solve(int u)
{
vis[u]=1;
for(int i=head[u];i;i=edge[i].next)
{
int v=edge[i].too;
if(vis[v])continue;
ma[rt=0]=sum=size[v];
getrt(v,0);
fa[rt]=u;
sz[rt]=size[v];//注意rt的大小是size[v]
T1[rt].init(size[v]);//注意不能全开n,否则变n^2
T2[rt].init(size[v]);
solve(rt);
}
}
void work(int x,int y)
{
for(int i=fa[x];i;i=fa[i])
{
int len=dis(x,i);
if(len<=sz[i])T1[i].change(len,y);
}
for(int i=x;i;i=fa[i])
{
if(fa[i]==0)break;
int len=dis(x,fa[i]);
if(len<=sz[i])T2[i].change(len,y);
}
}
signed main()
{
Log[0]=-1;
for(int i=1;i<=200000;i++)Log[i]=Log[i/2]+1;
n=read();m=read();
for(int i=1;i<=n;i++)a[i]=read();
for(int i=1;i<n;i++)
{
int x=read(),y=read();
add(x,y);
add(y,x);
}
dfs(1,0,0);
for(int i=1;i<=t;i++)st[i][0]=i;
for(int i=1;i<=Log[t];i++)
for(int j=1;j<=t-(1<<i)+1;j++)
{
int x=st[j][i-1],y=st[j+(1<<(i-1))][i-1];
if(dep[x]<=dep[y])st[j][i]=x;
else st[j][i]=y;
}
ma[rt=0]=sum=n;
getrt(1,0);
sz[rt]=n;
T1[rt].init(n);
T2[rt].init(n);
solve(rt);
for(int i=1;i<=n;i++)
work(i,a[i]);
while(m--)
{
int opt=read(),x=read()^lastans,y=read()^lastans;
if(opt==0)
{
int res=T1[x].find(y)+a[x];
//注意为了方便树状数组,没有把距离为0的点放进去
for(int i=x;i;i=fa[i])
{
if(fa[i]==0)break;
int len=dis(x,fa[i]);
if(y-len>=0)
{
res+=T1[fa[i]].find(y-len)+a[fa[i]];
res-=T2[i].find(y-len);
}
}
write(lastans=res);
putchar('\n');
}
else{
work(x,y-a[x]);
a[x]=y;
}
}
return 0;
}
/*
5 1
1 3 5 7 9
1 2
2 3
3 4
3 5
0 2 1
*/
/*
点分树最简单的写法就是维护两个数据结构(一般是vector)
一个记录点分树上以点u为根的子树与u的信息(到u的距离)
一个记录点分树上以点u为根的子树与fa[u]的信息(到法fa[u]的距离)
然后就可以通过容斥(减一减),求出在fa[u]的子树中且不在u的子树中的信息(到某某点的距离)
除了这种写法,就要重构树(使度数<=3)了
这题还有一个难点就是这两个数据结构是树状数组
那么就要动态开点(vector),有点麻烦
*/ 14.图论
1.最短路
2.最小生成树
3.拓扑排序
4.差分约束
条件形如:,我们可以看成
向
连了一条长度为
的有向边,若存在正环则无解。
有源点,就从源点spfa。
void spfa()
{
while(l<=r)
{
int u=q[l++];
vis[u]=false;
cnt[u]++;
if(cnt[u]==n){cout<<-1;exit(0);}
for(int i=head[u];i;i=edge[i].next)
{
int v=edge[i].too,w=edge[i].zh;
if(dis[v]<dis[u]+w)
{
dis[v]=dis[u]+w;
if(!vis[v])
{
q[++r]=v;
vis[v]=true;
}
}
}
}
}
for(int i=1;i<=m;i++)
{
scanf("%d%d%d",&x,&a,&b);
if(x==1){add(a,b,0);add(b,a,0);}
else if(x==2)add(a,b,1);
else if(x==3)add(b,a,0);
else if(x==4)add(b,a,1);
else if(x==5)add(a,b,0);
if(x%2==0&&a==b)
{
cout<<-1;
return 0;
}
}
l=1;
for(int i=1;i<=n;i++)
{
dis[i]=1;
vis[i]=true;
q[++r]=i;
}
spfa(); 这题没源点,那就每个联通块spfa一次
for(int i=1;i<=m;i++)
{
int x=read(),y=read(),z=read();
add(x-1,y,z);
add(y,x-1,-z);
}
for(int i=0;i<=n;i++)
if(gs[i]==0)
{
if(!spfa(i))
{
puts("false");
goto xjh;
}
}
puts("true");
xjh:; 5.欧拉回路
只要在dfs时加一个走过的边不走的优化,时间复杂度就是
void dfs(int x)
{
for(int &i=head[x];i;i=edge[i].next)
{
int v=edge[i].too;
if(vis[x][v])continue;
vis[x][v]=vis[v][x]=1;
dfs(v);
}
c[t--]=x;
}
int find(int x)
{
if(fa[x]==x)return x;
fa[x]=find(fa[x]);
return fa[x];
}
int main()
{
scanf("%d",&n);
for(int i=0;i<=500;i++)fa[i]=i;
for(int i=1;i<=n;i++)
{
cin>>a>>b;
g[a][b]=g[b][a]=1;
int xx=find(a),yy=find(b);
fa[xx]=yy;
du[a]++;
du[b]++;
}
for(int i=0;i<=500;i++)
for(int j=500;j>=0;j--)
if(g[i][j])add(i,j);
for(int i=0;i<=500;i++)
if(fa[i]==i&&du[i])k++;
if(k!=1){puts("No Solution");return 0;}
k=0;
for(int i=0;i<=500;i++)
if(du[i]%2==1)
{
k++;
if(!S)S=i;
}
if(S==0)
for(int i=0;i<500;i++)
if(du[i]){S=i;break;}
if(k!=0&&k!=2){puts("No Solution");return 0;}
t=n;
dfs(S);
printf("%c",S);
for(int i=1;i<=n;i++)printf("%c",c[i]);
return 0;
} 6.二分图
P1894 [USACO4.2]完美的牛栏The Perfect Stall
bool find(int x)
{
for(int i=head[x];i;i=edge[i].next)
{
int v=edge[i].too;
if(!b[v])
{
b[v]=true;
if(!a[v]||find(a[v]))
{
a[v]=x;
return true;
}
}
}
return false;
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
{
scanf("%d",&k);
for(int j=1;j<=k;j++)
{
scanf("%d",&x);
add(i,x+n);
}
}
for(int i=1;i<=n;i++)
{
memset(b,false,sizeof(b));
if(find(i))ans++;
}
cout<<ans;
return 0;
} 7.网络流
信息学竞赛


