2025.2.3-2025.2.9学习笔记
前言
不知不觉寒假集训已经过去一周了,学的东西感觉比正常学期内一两个月都多……再不写写总结估计就白学了(已经白学了)。
1.直径重心LCA
下面内容均指树
直径与重心:是不是谁之前写过,不管了我不写了。
直径是树上最长的一条链。
重心是去掉该点可以使剩余连通块的点数相差最小。
下面是LCA
方法一:倍增求LCA
原理就是先使一个点通过跳 \(2^k\) 的距离,先到达与另一点相同高度,随后两个点同时跳 \(2^k\) 的距离,找到公共祖先。需要预先求出每个点的深度 \(dep\)
注意:当两个点同时上移时,不应一下跳到最高点,而是跳小于两点距 LCA 的最大 \(2^k\) 的距离。(似乎有点过于抽象了)
下面是代码试块:
void dfs(int u,int fa){
f[u][0]=fa;
dep[u]=dep[fa]+1;
for(int i=1;(1<<i)<=dep[u];i++){
f[u][i]=f[f[u][i-1]][i-1];
}
for(int i=0;i<(int)vec[u].size();i++){
int v=vec[u][i];
if(v==fa) continue;
dfs(v,u);
}
return;
}
int Lca(int u,int v){
int temp;
if(dep[u]<dep[v]) swap(u,v);
temp=dep[u]-dep[v];
for(int i=0;(1<<i)<=temp;i++){
if((1<<i)&temp){
u=f[u][i];
}
}
if(u==v){
return u;
}
for(int i=30;i>=0;i--){
if(f[u][i]!=f[v][i]){
u=f[u][i];
v=f[v][i];
}
}
return f[u][0];
}
方法二:Tarjan离线求LCA
1.以s为根节点,从根节点开始。
2.遍历该点u所有子节点v,并标记这些子节点v已被访问过。
3.若是v还有子节点,返回2,否则下一步。
4.合并v到u上。
5.寻找与当前点u有询问关系的点v。
6.若是v已经被访问过了,则可以确认u和v的最近公共祖先为v被合并到的父亲节点a。
--摘自题解区(因为我不会解释)
下面是代码:
#include<iostream>
#include<vector>
using namespace std;
int n,m,s;
int fa[500010];
int vis[500010];
struct node{
int to,nxt;
}e[1000010];
int head[500010],tot;
vector<int> ask[500010];
vector<int> id[500010];
int ans[500010];
int aa[500010];
void add(int u,int v){
e[++tot].to=v;
e[tot].nxt=head[u];
head[u]=tot;
return;
}
int find(int x){
if(fa[x]==x) return x;
else return fa[x]=find(fa[x]);
}
void work(int u){
vis[u]=1;
for(int i=head[u];i;i=e[i].nxt){
int v=e[i].to;
if(vis[v]) continue;
work(v);
fa[v]=u;
}
for(int i=0;i<(int)ask[u].size();i++){
int v=ask[u][i];
if(vis[v]==2){
ans[id[u][i]]=find(v);
}
}
vis[u]=2;
return;
}
int main(){
cin>>n>>m>>s;
for(int i=1;i<=n;i++) fa[i]=i;
for(int i=1;i<n;i++){
int x,y;
cin>>x>>y;
add(x,y);
add(y,x);
}
for(int i=1;i<=m;i++){
int a,b;
cin>>a>>b;
if(a==b){
ans[i]=a;
}
ask[a].push_back(b);
id[a].push_back(i);
ask[b].push_back(a);
id[b].push_back(i);
}
work(s);
for(int i=1;i<=m;i++){
cout<<ans[i]<<endl;
}
return 0;
}
方法三:树剖求LCA
太高级了,同样是 \(O(\log n)\) 的复杂度,但是比倍增快正 \(\inf\) 倍。
考虑我们树剖完后的链,对于两个点,当它们跳到一条链时,深度小的点就是他们的 LCA 。
然后就是暴力跳链的过程。
code:
#include<iostream>
#include<queue>
#include<algorithm>
#include<cstring>
#include<vector>
using namespace std;
const int N=5e5+50;
const int M=5e5+50;
int n,m,s;
struct Edge{
int to,nxt;
}e[2*M];
int head[2*M],tot;
void add(int u,int v){
e[++tot].to=v;
e[tot].nxt=head[u];
head[u]=tot;
return ;
}
int dep[N],hson[N],siz[N],top[N],fa[N];
void dfs1(int u,int f,int cnt){
dep[u]=dep[f]+1;
siz[u]=1;
fa[u]=f;
for(int i=head[u];i;i=e[i].nxt){
int v=e[i].to;
if(v==f) continue;
dfs1(v,u,cnt+1);
siz[u]+=siz[v];
if(siz[v]>siz[hson[u]]){
hson[u]=v;
}
}
return ;
}
void dfs2(int u,int tp){
top[u]=tp;
if(hson[u]){
dfs2(hson[u],tp);
}
for(int i=head[u];i;i=e[i].nxt){
int v=e[i].to;
if(v==fa[u] || v==hson[u]) continue;
dfs2(v,v);
}
}
int lca(int x,int y){
while(top[x]!=top[y]){
if(dep[top[x]]>dep[top[y]])
x=fa[top[x]];
else
y=fa[top[y]];
}
return dep[x]>dep[y]?y:x;
}
int main(){
ios::sync_with_stdio(0);
cin.tie(0);cout.tie(0);
cin>>n>>m>>s;
for(int i=1;i<=n-1;i++){
int u,v;
cin>>u>>v;
add(u,v);
add(v,u);
}
dfs1(s,0,0);
dfs2(s,s);
for(int i=1;i<=m;i++){
int x,y;
cin>>x>>y;
cout<<lca(x,y)<<'\n';
}
return 0;
}
这里的题怎么啥也没做啊!!!
已补,详见后面的做题记录。
2.DP杂题
考虑决策集合的性质,发现经过细心构造可以使决策中的每一个元素都以最小的附加价获得。定义 \(dp{_i}{_,}{_j}\) 为前 \(i\) 个物品取 \(j\) 个的最小花费,转移方程见代码(不想敲KateX了)
AC code:
#include<iostream>
#include<cstring>
using namespace std;
int n,m;
int a[5010];
int c[5010];
int x[5010];
bool mus[5010];
long long dp[5010][5010];
long long ans=1e18;
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>a[i];
}
for(int i=1;i<=n;i++){
cin>>c[i];
}
for(int i=1;i<=m;i++){
cin>>x[i];
mus[x[i]]=1;
}
memset(dp,0x3f,sizeof(dp));
dp[0][0]=0;
for(int i=1;i<=n;i++){
int minn=c[i];
for(int j=0;j<=i;j++){
minn=min(c[i-j],minn);
if(!mus[i]){
dp[i][j]=min(dp[i][j],dp[i-1][j]);
}
if(j<i){
dp[i][j+1]=dp[i-1][j]+a[i]+minn;
}
}
}
for(int i=m;i<=n;i++){
ans=min(ans,dp[n][i]);
}
cout<<ans;
return 0;
}
题目简述大致是:给定序列,规定不能取序列中的某一段,使剩余段凑出给出值。
考虑将输入做上述处理,发现可以以合法的 \(a\) 来钦定 \(b\) 的范围,做背包验证答案,时间复杂度劣飞了。
考虑重新定义背包含义为:当体积为 \(k\) 时,\(b\) 的最小值,此时就需要将询问离线并以左端点升序。
AC code:
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
int n,m;
struct node{
int a,b,c;
}s[10010];
struct Ask{
int l,k,r;
int id;
}q[1050000];
bool cmp(node a,node b){
return a.a<b.a;
}
bool cmp2(Ask a,Ask b){
if(a.l==b.l) return a.r<b.r;
return a.l<b.l;
}
bool cmp3(Ask a,Ask b){
return a.id<b.id;
}
int f[100010];
bool ans[1000010];
int main(){
ios::sync_with_stdio(0);
cin.tie(0);cout.tie(0);
cin>>n;
for(int i=1;i<=n;i++){
cin>>s[i].c>>s[i].a>>s[i].b;
}
cin>>m;
for(int i=1;i<=m;i++){
cin>>q[i].l>>q[i].k>>q[i].r;
q[i].r=q[i].l+q[i].r;
q[i].id=i;
}
sort(s+1,s+1+n,cmp);
sort(q+1,q+1+m,cmp2);
int tot=1;
f[0]=1e9;
for(int i=1;i<=m;i++){
while(tot<=n && s[tot].a<=q[i].l){
for(int k=100000;k>=s[tot].c;k--){
f[k]=max(f[k],min(f[k-s[tot].c],s[tot].b));
}
tot++;
}
if(f[q[i].k]>q[i].r) ans[q[i].id]=1;
}
for(int i=1;i<=m;i++){
if(ans[i])cout<<"TAK"<<endl;
else cout<<"NIE"<<endl;
}
return 0;
}
发现在第 \(i\) 行 第 \(j\) 列架设桥墩的代价是:\(dp_{i,j}=a{_i}{_,}{_j}+1+min_{j=i-d+1}^{i}(dp_{i,j})\) 那么需要单调队列维护决策区间的最值。
AC code:
#include<iostream>
#include<deque>
#define int long long
using namespace std;
int t;
int n,m,k,d;
int f[200010];
int a[110][200010];
int ans[110];
signed main(){
cin>>t;
while(t--){
cin>>n>>m>>k>>d;
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
cin>>a[i][j];
}
}
for(int i=1;i<=n;i++){
deque<int> q;
q.clear();
f[1]=a[i][1]+1;
q.push_back(1);
for(int j=2;j<=m;j++){
while(!q.empty()&&j-q.front()>d+1) q.pop_front();//排除过时决策
f[j]=f[q.front()]+a[i][j]+1;
while(!q.empty()&&f[q.back()]>f[j]) q.pop_back();//排除不可能决策
q.push_back(j);//添加决策
}
ans[i]=ans[i-1]+f[m];
}
long long fans=ans[k];
for(int i=k+1;i<=n;i++){
fans=min(fans,ans[i]-ans[i-k]);
}
cout<<fans<<endl;
}
return 0;
}
下辈子还得重刷
3.贪心二分
大胆猜大胆写就完了
分数规划。考虑将原式变形成一个 \(a-b\times x > 0\) 之类的形式,二分 \(x\) 然后去 \(check(x)\) 的合理性,若 \(x\ge 0\) 表示这个可以继续增长。
AC code:
#include<iostream>
#include<cstring>
using namespace std;
int n,W;
const long long inf=1e18;
struct node{
long long w,t;
}c[500];
long long f[1010];
bool check(int x){
for(int i=1;i<=W;i++) f[i]=-inf;
f[0]=0;
for(int i=1;i<=n;i++){
for(int j=W;j>=0;j--){
if(f[j]==-inf) continue;
int k=j+c[i].w;
k=min(k,W);
f[k]=max(f[k],f[j]+c[i].t-x*c[i].w);
}
}
return f[W]>=0;
}
int main(){
cin>>n>>W;
for(int i=1;i<=n;i++){
cin>>c[i].w>>c[i].t;
c[i].t*=1000;
}
int l=0,r=1000000;
while(l<=r){
int mid=(l+r)>>1;
if(check(mid)){
l=mid+1;
}else{
r=mid-1;
}
}
cout<<r;
return 0;
}
给定一棵树,选择 \(l\) 条路径使覆盖的点最多的点数。
观察到对于叶子结点,我们每一条路径都可以通过走到两个点的 LCA 把它们同时覆盖。扩展发现我们可以每次从叶子节点走回根节点,再往叶子节点出发,每一层能取到的叶子数就是 \(min(2*l,当前层的叶子数)\) ,接下来只需要跑一遍拓扑分层计数即可。
AC code:
#include<iostream>
#include<queue>
#include<algorithm>
using namespace std;
int n,k;
long long ans=0;
struct node{
int to,nxt;
}e[2000010];
struct node2{
int id,cnt;
}s[2000010];
int head[1000010],tot;
int ind[1000010];
int sum[1000010];
int cnt=0;
queue<node2> que;
void add(int u,int v){
e[++tot].to=v;
e[tot].nxt=head[u];
head[u]=tot;
return;
}
void add2(int i,int cnt){
node2 a={i,cnt};
que.push(a);
}
int vis[1000010];
void topo(){
for(int i=1;i<=n;i++){
if(ind[i]==1){
vis[i]=1;
add2(i,1);
}
}
while(!que.empty()){
node2 u=que.front();que.pop();
for(int i=head[u.id];i;i=e[i].nxt){
ind[e[i].to]--;
if(ind[e[i].to]==1){
vis[e[i].to]=u.cnt+1;
add2(e[i].to,u.cnt+1);
}
}
}
}
int main(){
cin>>n>>k;
for(int i=1;i<n;i++){
int u,v;
cin>>u>>v;
add(u,v);
add(v,u);
ind[u]++;ind[v]++;
}
topo();
sort(vis+1,vis+1+n);
int old=0;
for(int i=1;i<=n;i++){
if(old!=vis[i]){
old=vis[i];
cnt++;
}
sum[cnt]++;
}
for(int i=1;i<=cnt;i++){
ans+=min(k*2,sum[i]);
}
cout<<ans;
return 0;
}
4.搜索
可行性与最优性剪枝考虑全面,尽量把复杂度变成 \(O(能过)\)。其他的逆天剪枝技巧全看题了吧。
放一道经典搜索:小木棍
能用的都用上了。
AC code:
#include<iostream>
#include<algorithm>
using namespace std;
int n,minl,sum;
int a[70],vis[70],nxt[70];
int cnt=0,m;
void dfs(int k,int s,int l,int las){
if(s==l){
if(k==m){cout<<l;exit(0);}
int i;
for(i=1;i<=cnt;i++) if(!vis[i]) break;
vis[i]=1;
dfs(k+1,a[i],l,1);
vis[i]=0;
}
int ll=las+1,rr=cnt;
while(ll<=rr){
int mid=(ll+rr)>>1;
if(a[mid]<=l-s) rr=mid-1;
else ll=mid+1;
}
for(int i=ll;i<=cnt;i++){
if(vis[i]) continue;
vis[i]=1;
dfs(k,s+a[i],l,i);
vis[i]=0;
if(l-s==a[i] || l-s==l) return;
i=nxt[i];
if(i==cnt) return;
}
return;
}
bool cmp(int a,int b){
return a>b;
}
int main(){
cin>>n;
for(int i=1;i<=n;i++){
int x;cin>>x;
if(x>50) continue;
a[++cnt]=x;
sum+=x;
minl=max(minl,a[i]);
}
sort(a+1,a+1+cnt,cmp);
nxt[cnt]=cnt;
for(int i=cnt-1;i>=1;i--){
if(a[i]==a[i+1]) nxt[i]=nxt[i+1];
else nxt[i]=i;
}
for(minl;minl<=sum/2;minl++){
if(sum%minl!=0) continue;
m=sum/minl;
vis[1]=1;
dfs(1,a[1],minl,1);
vis[1]=0;
}
cout<<sum;
return 0;
}
5.Tarjan
1. Tarjan缩点
考虑图中一些环,如果我们到达环上一个点时可以不付出任何代价就走完一个环,那么我们就可以把这个环缩成一个点。然后重新建边,最后整张图就换变成一张非常优美的DAG。
然后我们就可以为所欲为了。
下面是Tarjan缩点板子:
#include<iostream>
#include<algorithm>
#include<stack>
#include<queue>
using namespace std;
int n,m,a[100010];
int w[100010];
int dfn[100010],low[100010],dfx;
bool vis[100010];
stack<int> sta;
queue<int> que;
struct node{
int to,nxt,from;
}e[100010],e2[100010];
int head[100010],tot;int head2[100010],tot2;
int co[100010];
int in[100010];
int dis[100010];
void add(int u,int v){
e[++tot].to=v;
e[tot].from=u;
e[tot].nxt=head[u];
head[u]=tot;
return;
}
void build(int u,int v){
e2[++tot2].to=v;
e2[tot2].from=u;
e2[tot2].nxt=head2[u];
head2[u]=tot2;
return;
}
void tarjan(int u){
dfn[u]=low[u]=++dfx;
vis[u]=1;
sta.push(u);
for(int i=head[u];i;i=e[i].nxt){
int v=e[i].to;
if(!dfn[v]){
tarjan(v);
low[u]=min(low[v],low[u]);
}else if(vis[v]){
low[u]=min(low[v],low[u]);
}
}
if(low[u]==dfn[u]){
co[u]=u;
vis[u]=0;
while(sta.top()!=u){
co[sta.top()]=u;
vis[sta.top()]=0;
w[u]+=w[sta.top()];
sta.pop();
}
sta.pop();
}
return;
}
int topo(){
for(int i=1;i<=n;i++){
if(co[i]==i && !in[i]){
que.push(i);
dis[i]=w[i];
}
}
while(!que.empty()){
int u=que.front();que.pop();
for(int i=head2[u];i;i=e2[i].nxt){
int v=e2[i].to;
dis[v]=max(dis[v],dis[u]+w[v]);
in[v]--;
if(in[v]==0) que.push(v);
}
}
int ans=0;
for(int i=1;i<=n;i++){
ans=max(ans,dis[i]);
}
return ans;
}
int main(){
ios::sync_with_stdio(0);
cin.tie(0);cout.tie(0);
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>w[i];
}
for(int i=1;i<=m;i++){
int u,v;
cin>>u>>v;
add(u,v);
}
for(int i=1;i<=n;i++){
if(!dfn[i])
tarjan(i);
}
for(int i=1;i<=m;i++){
int u=co[e[i].from],v=co[e[i].to];
if(u!=v){
build(u,v);
in[v]++;
}
}
cout<<topo();
return 0;
}
2.Tarjan割点
割点指删去该点能使一张连通图变为不连通的点。(废话)
下面是割点板子:
#include<iostream>
using namespace std;
int n,m,root;
int dfn[20010],low[20010],dfx;
int head[200010],tot;
struct node{
int to,nxt;
}e[200010];
void add(int u,int v){
e[++tot].to=v;
e[tot].nxt=head[u];
head[u]=tot;
return;
}
int ans,vis[20010];
void tarjan(int u){
dfn[u]=low[u]=++dfx;
int col=0;
for(int i=head[u];i;i=e[i].nxt){
int v=e[i].to;
if(!dfn[v]){
col++;
tarjan(v);
low[u]=min(low[u],low[v]);
if((u==root && col>1)||(u!=root&&dfn[u]<=low[v])){
vis[u]=1;
}
}else{
low[u]=min(low[u],dfn[v]);
}
}
}
int main(){
cin>>n>>m;
for(int i=1;i<=m;i++){
int u,v;
cin>>u>>v;
add(u,v);
add(v,u);
}
for(int i=1;i<=n;i++){
if(!dfn[i]){
root=i;
tarjan(i);
}
}
for(int i=1;i<=n;i++){
if(vis[i]) ans++;
}
cout<<ans<<endl;
for(int i=1;i<=n;i++){
if(vis[i]) cout<<i<<' ';
}
return 0;
}
6.图论杂题
做的不多,而且感觉非常神秘。
\(N<50\) 的数据范围考虑 Floyd 求全源最短路,发现最短路径并不是最小密度路径。进而想到添加一维状态为路径长度,进行一个dp。\(dp{_i}{_,}{_j}{_,}{_k}\) 表示从 \(i\) 到 \(j\) 通过 \(k\) 条路径的最短路。
AC code:
#include<iostream>
#include<iomanip>
using namespace std;
const int inf = 1e9;
const int maxn=1e5+10;
int n,m,q;
int head[1010],tot;
int f[60][60][1010];
void init(){
for(int i=1;i<=m;i++){
for(int j=1;j<=n;j++){
for(int k=1;k<=n;k++){
f[j][k][i]=inf;
}
}
}
}
int main(){
cin>>n>>m;
init();
for(int i=1;i<=m;i++){
int a,b,w;
cin>>a>>b>>w;
if(f[a][b][1]>w){
f[a][b][1]=w;
}
}
for(int l=2;l<=m;l++){
for(int mid=1;mid<=n;mid++){
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
f[i][j][l]=min(f[i][j][l],f[i][mid][l-1]+f[mid][j][1]);
}
}
}
}
cin>>q;
for(int i=1;i<=q;i++){
int u,v;
cin>>u>>v;
double ans=inf;
for(int j=1;j<=n;j++){
if(f[u][v][j]<inf)
ans=min(ans,(double)f[u][v][j]/(double)j);
}
if(ans==inf)
cout<<"OMG!"<<endl;
else
cout<<fixed<<setprecision(3)<<ans<<endl;
}
return 0;
}
本题可将相连两点的点权较大值定义为两点的边权,随后按照边权升序排序,建立 kruskal 重构树。对于本题来说:两点的点权之和为新节点的点权,新节点作为两点的父亲节点。所以这颗 kruskal 重构树应为一颗二叉树。依据题的条件:如果 A村庄 想说服 B村庄 ,那么 A村庄 的子树和应大于 B村庄 的点权。跑一遍 DFS 。我们就做完了。
AC code:
#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;
const int maxn=2e5+10;
const int inf=1e9;
int n,m,tot;
long long num[2*maxn],val[2*maxn];
int fa[maxn*2+10];
int ans[maxn*2];
vector<int> vec[2*maxn];
struct node{
int x,y;
long long w;
}e[maxn];
bool cmp(node a,node b){
return a.w<b.w;
}
int find(int x){
if(x==fa[x]) return x;
else return fa[x]=find(fa[x]);
}
void dfs(int u,int f){
if(num[u]>=val[f]){
ans[u]=(ans[u]||ans[f]);
}
for(int i=0;i<(int)vec[u].size();i++){
if(vec[u][i]==f) continue;
dfs(vec[u][i],u);
}
}
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>num[i];
}
for(int i=1;i<=m;i++){
cin>>e[i].x>>e[i].y;
e[i].w=max(num[e[i].x],num[e[i].y]);
}
for(int i=1;i<=2*n;i++){
fa[i]=i;
}
sort(e+1,e+1+m,cmp);
tot=n;
for(int i=1;i<=m;i++){
int x=find(e[i].x);
int y=find(e[i].y);
if(x==y) continue;
fa[x]=fa[y]=++tot;
num[tot]=num[x]+num[y];
val[tot]=e[i].w;
vec[tot].push_back(x);
vec[tot].push_back(y);
}
ans[tot]=1;//根节点一定能赢
dfs(tot,tot);
for(int i=1;i<=n;i++){
cout<<ans[i];
}
return 0;
}
非常神秘的题,观察性质然后暴力瞎搞。
--jzp学长原话
优先考虑暴力,对于每个点都以其为根节点,儿子向父亲建边,跑拓扑,发现环即为无解,反之有解。
发现对于关系 \((a,b)\) \(a\) 需要比 \(b\) 先离场,那么 \(a\) 的子树以及 \(a\) 节点一定不能作为根,那么直接删去 \(a\) 的子树。随后对于剩下的点再进行拓扑,判断它们是否有环,然后输出答案即可。(中途遇到环的情况直接输出 \(0\) ,并终止程序。)
#include<iostream>
#include<queue>
#include<vector>
using namespace std;
const int maxn=2e5+50;
int n,m;
queue<int> que;
vector<int> e[maxn];
vector<int> r[maxn];
int d[maxn];
bool vis[maxn];
bool ans[maxn];
int root;
int work(){
int cnt=0;
while(!que.empty()){
int u=que.front();que.pop();
++cnt;
if(d[u]!=1){
if(d[u]<1) return u;
for(int i=1;i<=n;i++){
cout<<0<<endl;
}
exit(0);
}
for(int i=0;i<(int)e[u].size();i++){
int v=e[u][i];
cnt++;
d[v]--;
if(d[v]==1) que.push(v);
}
for(int i=0;i<(int)r[u].size();i++){
int v=r[u][i];
cnt++;
d[v]--;
if(d[v]==1) que.push(v);
}
}
if(cnt!=n){
for(int i=1;i<=n;i++){
cout<<0<<endl;
}
exit(0);
}
}
void dfs(int u,int fa){
ans[u]=1;
for(int i=0;i<(int)e[u].size();i++){
if(fa==e[u][i] || vis[e[u][i]]) continue;
dfs(e[u][i],u);
}
}
int main(){
cin>>n>>m;
for(int i=1;i<n;i++){
int u,v;
cin>>u>>v;
e[u].push_back(v);
e[v].push_back(u);
d[u]++;d[v]++;
}
for(int i=1;i<=m;i++){
int u,v;
cin>>u>>v;
r[u].push_back(v);
vis[u]=1;
d[v]++;
}
for(int i=1;i<=n;i++){
if(d[i]==1) que.push(i);
}
root=work();
dfs(root,-1);
for(int i=1;i<=n;i++){
cout<<ans[i]<<'\n';
}
return 0;
}
7.学抽象DS,品赤石人生
1.ST表
利用倍增静态维护区间最大,最小值的数据结构。
原理是把一个区间分成两个 \(2^k\) 长度的区间,用两个区间的信息得出该区间的信息。
\(O(nlog_n)\) 预处理,\(O(1)\) 查询的优秀复杂度。
板子:
#include<iostream>
using namespace std;
int n,m;
int a[1000010];
int st[1000010][40];
int lg2[1000010];
int ask(int l,int r){
int j=lg2[r-l+1];
return max(st[l][j],st[r-(1<<j)+1][j]);
}
int main(){
ios::sync_with_stdio(0);
cin.tie(0);cout.tie(0);
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>a[i];
st[i][0]=a[i];
}
lg2[1]=0;
for(int i=2;i<=n;i++){
lg2[i]=lg2[i/2]+1;
}
for(int j=1;(1<<j)<=n;j++){
for(int i=1;i+(1<<j)-1<=n;i++){
st[i][j]=max(st[i][j-1],st[i+(1<<(j-1))][j-1]);
}
}
for(int i=1;i<=m;i++){
int l,r;cin>>l>>r;
cout<<ask(l,r)<<'\n';
}
return 0;
}
这里的例题比较明显,当我们发现题目需要查询区间最值且静态查询时,就可以考虑 ST 表了。
发现需要维护区间最大子段和,考虑对前缀和数组建立 ST 表,然后用堆维护最优解,注意一个区间内的次优解,可能是除该区间外的最优解,所以每次取完区间最优解后还需要将区间左右两侧再次放入堆中等待决策。
AC code:
#include<bits/stdc++.h>
using namespace std;
int n,k,L,R;
int a[501000];
int st[501000][50];
int d[501000][50];
int lg2[501000];
int arr[501000];
struct node{
int sum,sd,id;
//sum=当前的字段和
//sd=开始的点
//id=l-r区间内选的最小值的点
int ll,rr;
//ll=以st为末尾长度为ll
//rr=以st为末尾长度为rr
friend bool operator < (struct node a,struct node b) {
return a.sum < b.sum;
}
};
priority_queue<node,vector<node>,less<node> > q;
int ask2(int l,int r){
int j=lg2[r-l+1];
if(st[l][j]>st[r-(1<<j)+1][j]){
return d[l][j];
}else{
return d[r-(1<<j)+1][j];
}
}
void init(){
cin>>n>>k>>L>>R;
for(int i=1;i<=n;i++){cin>>a[i];arr[i]=arr[i-1]+a[i];d[i][0]=i;if(i>=2) lg2[i]=lg2[i/2]+1;}
for(int i=1;i<=n;i++) st[i][0]=arr[i];
for(int j=1;(1<<j)<=n;j++){
for(int i=1;i+(1<<j)-1<=n;i++){
if(st[i][j-1]<st[i+(1<<(j-1))][j-1]){
st[i][j]=st[i+(1<<(j-1))][j-1];
d[i][j]=d[i+(1<<(j-1))][j-1];
}else{
st[i][j]=st[i][j-1];
d[i][j]=d[i][j-1];
}
}
}
return;
}
void add(int w,int i,int l,int r){
node d;
d.sum=arr[w]-arr[i-1];
d.rr=r;d.ll=l;
d.id=i;d.sd=w;
q.push(d);
}
int main(){
ios::sync_with_stdio(0);cin.tie(0),cout.tie(0);
init();
for(int i=1;i+L-1<=n;i++){
add(ask2(i+L-1,min(i+R-1,n)),i,i+L-1,min(i+R-1,n));
}
long long ans=0;
for(int i=1;i<=k;i++){
node k=q.top();
q.pop();
ans+=k.sum;
int st=k.sd,j=k.id;
int l=k.ll,r=k.rr;
if(st>l){
add(ask2(l,st-1),j,l,st-1);
}
if(st<r){
add(ask2(st+1,r),j,st+1,r);
}
}
cout<<ans;
return 0;
}
/*做前缀和后求最大不定长字段和(利用st表)
利用优先队列存可能的最优解
每次取队头的值进行累加
*/
2.树状数组
抽象至极的数据结构。
lowbit 是精髓。
板子:
#include<iostream>
using namespace std;
int lowbit(int x){return x&-x;}
long long tree[500010],a[500010];
int n,m;
void add(int p,int x){
while(p<=n){
tree[p]+=x;
p+=lowbit(p);
}
return;
}
long long sum(int p){
long long res=0;
while(p){
res+=tree[p];
p-=lowbit(p);
}
return res;
}
int main(){
ios::sync_with_stdio(0);
cin.tie(0);cout.tie(0);
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>a[i];
add(i,a[i]);
}
for(int i=1;i<=m;i++){
int w;cin>>w;
if(w==1){
int x,p;
cin>>p>>x;
add(p,x);
}else{
int l,r;
cin>>l>>r;
cout<<sum(r)-sum(l-1)<<"\n";
}
}
return 0;
}
树状数组可用来优化最长公共子序列。
用 vector 记录每个点出现的位置。对于每一个 \(a_i\) 遍历与其相同的 \(b_j\) 那么以 \(a_j\) 与 \(b_j\) 为结尾的最长公共子序列长度为:\(b\) 序列中 \(b_j\) 前的最长公共子序列的最大值,那么就需要倒序遍历 \(b\) 序列。
AC code:
#include<iostream>
#include<queue>
#include<algorithm>
#include<cstring>
#include<vector>
using namespace std;
const int maxn=1e6+50;
int n,tr[maxn],a[maxn],b[maxn];
int l[maxn];
vector<int> vec[maxn];
int lowbit(int x){return x&-x;}
void add(int p,int x){
while(p<=n){
tr[p]=max(x,tr[p]);
p+=lowbit(p);
}
}
int ask(int p){
int res=0;
while(p){
res=max(res,tr[p]);
p-=lowbit(p);
}
return res;
}
int main(){
cin>>n;
for(int i=1;i<=n;i++){
cin>>a[i];
}
for(int i=1;i<=n;i++){
cin>>b[i];
vec[b[i]].push_back(i);
}
for(int i=1;i<=n;i++){
for(int j=vec[a[i]].size()-1;j>=0;j--){
int mx=-1;
mx=max(mx,ask(vec[a[i]][j]-1)+1);
add(vec[a[i]][j],mx);
l[i]=max(l[i],mx);
}
}
int ans=-1;
for(int i=1;i<=n;i++){
ans=max(ans,l[i]);
}
cout<<ans;
return 0;
}
同样可以来优化最长不下降子序列
先将 \(a\) 序列递增排序,树状数组中一编号为下标存储以每个下标为结尾的最长上升子序列长度,将 \(a\) 接入后面。
#include<iostream>
#include<queue>
#include<algorithm>
#include<cstring>
#include<vector>
using namespace std;
const int N=1e4+50;
int n;
struct node{
int v,id;
bool operator <(const node &a){
if(v==a.v) return id>a.id;
return v<a.v;
}
}a[N];
int tr[1000010];
int lowbit(int p){return p&-p;}
void add(int p,int x){
while(p<=n){
tr[p]=max(x,tr[p]);
p+=lowbit(p);
}
return ;
}
int count(int p){
int res=0;
while(p){
res=max(res,tr[p]);
p-=lowbit(p);
}
return res;
}
int main(){
cin>>n;
for(int i=1;i<=n;i++){
cin>>a[i].v;
a[i].id=i;
}
int ans=0;
sort(a+1,a+1+n);
for(int i=1;i<=n;i++){
add(a[i].id,count(a[i].id)+1);
}
cout<<count(n);
return 0;
}
3.线段树
非常全能的数据结构。(代码长度与功能成正比,时间复杂度与功能成正比)。
注意多种操作时每个操作的优先级。(赋值一般是最优先的)当有高等级操作进行时,同时进行低等级操作并清空低等级操作tag。
板子2:#马蜂致歉
#include<iostream>
#define int long long
using namespace std;
int n,m;const int p=571373;
int a[400010],tree[400010],tag[400010],tag2[400010];
void pushup(int u){
tree[u]=tree[u*2]%p+tree[u*2+1]%p;
tree[u]%=p;
}
void pushdown(int u,int l,int r){
int mid=(l+r)/2;
tree[u*2]=(tree[u*2]*tag2[u]+tag[u]*(mid-l+1))%p;
tree[u*2+1]=(tree[u*2+1]*tag2[u]+tag[u]*(r-mid))%p;
tag2[u*2]=tag2[u*2]*tag2[u]%p;
tag2[u*2+1]=tag2[u*2+1]*tag2[u]%p;
tag[u*2]=(tag[u*2]*tag2[u]+tag[u])%p;
tag[u*2+1]=(tag[u*2+1]*tag2[u]+tag[u])%p;
tag[u]=0;tag2[u]=1;
return;
}
void add(int u,int l,int r,int x,int y,int k){
if(l>y || r<x) return;
if(l>=x && r<=y){
tag[u]=(tag[u]%p+k%p)%p;
tree[u]=(tree[u]%p+k*(r-l+1)%p)%p;
return;
}
pushdown(u,l,r);
int mid=(l+r)/2;
add(u*2,l,mid,x,y,k);
add(u*2+1,mid+1,r,x,y,k);
pushup(u);
return;
}
void mull(int u,int l,int r,int x,int y,int k){
if(l>y || r<x) return ;
if(l>=x && r<=y){
tree[u]=tree[u]*k%p;
tag[u]=tag[u]*k%p;
tag2[u]=tag2[u]*k%p;
return;
}
pushdown(u,l,r);
int mid=(l+r)/2;
mull(u*2,l,mid,x,y,k);
mull(u*2+1,mid+1,r,x,y,k);
pushup(u);
return;
}
void build(int u,int l,int r){
if(l==r){
tree[u]=a[l];
tree[u]%=p;
return ;
}
int mid=(l+r)/2;
build(u*2,l,mid);
build(u*2+1,mid+1,r);
pushup(u);
return;
}
int ask(int u,int l,int r,int x,int y){
if(l>y || r<x) return 0;
if(l>=x && r<=y){
return tree[u]%p;
}
int mid=(l+r)/2;
pushdown(u,l,r);
return (ask(u*2,l,mid,x,y)%p+ask(u*2+1,mid+1,r,x,y)%p)%p;
}
signed main(){
// freopen("in.txt","r",stdin);
int aaa;
cin>>n>>m>>aaa;
tag2[1]=1;
for(int i=1;i<=n;i++){
cin>>a[i];
add(1,1,n,i,i,a[i]);
}
for(int i=1;i<=m;i++){
int w;cin>>w;
if(w==1){
int x,y,k;
cin>>x>>y>>k;
mull(1,1,n,x,y,k);
}else if(w==2){
int x,y,k;
cin>>x>>y>>k;
add(1,1,n,x,y,k);
}else{
int x,y;
cin>>x>>y;
cout<<ask(1,1,n,x,y)<<'\n';
}
}
return 0;
}
动态开点线段树
秉持用多少开多少的理念,就有了动态开点线段树。
具体空间复杂度类似 \(O(nlog_n)\)
对于本题来说,空间复杂度即为 \(O(qlog_n)\) 。
具体的操作与正常线段树几乎一致,\(u\times 2\) 变为
\(tr[u].lc\) 即可(右子树同理)。
AC code:
#include<iostream>
#include<cstring>
using namespace std;
int n,q,l,r,tot;
int root;
struct node{
int d,lc,rc;
}t[15001000];
int tag[15001000];
int build(){
tot++;
t[tot].d=t[tot].lc=t[tot].rc=0;
return tot;
}
void pushdown(int u,int l,int r){
if(tag[u]==-1) return;
int mid=(l+r)/2;
if(!t[u].lc) t[u].lc=build();
if(!t[u].rc) t[u].rc=build();
t[t[u].lc].d=tag[u]*(mid-l+1);
t[t[u].rc].d=tag[u]*(r-mid);
tag[t[u].lc]=tag[u];
tag[t[u].rc]=tag[u];
tag[u]=-1;
return;
}
void add(int u,int l,int r,int x,int y,int k){
if(l>y || r<x) return;
if(r<=y&&l>=x){
t[u].d=k*(r-l+1);
tag[u]=k;
return;
}
pushdown(u,l,r);
int mid=(l+r)/2;
if(!t[u].lc) t[u].lc=build();
add(t[u].lc,l,mid,x,y,k);
if(!t[u].rc) t[u].rc=build();
add(t[u].rc,mid+1,r,x,y,k);
t[u].d=t[t[u].lc].d+t[t[u].rc].d;
return;
}
int main(){
ios::sync_with_stdio(0);
cin.tie(0);cout.tie(0);
cin>>n>>q;
memset(tag,-1,sizeof(tag));
build();
for(int i=1;i<=q;i++){
int l,r,k;
cin>>l>>r>>k;
if(k==1)
add(1,1,n,l,r,1);
else
add(1,1,n,l,r,0);
cout<<n-t[1].d<<'\n';
}
return 0;
}
喜欢 DS 中的大模拟吗?
本题需要维护多达 \(8\) 个区间信息,做完后对于基本的线段树理解可以说是较为优秀了。
AC code:
#include<iostream>
#include<queue>
#include<algorithm>
#include<cstring>
#define lc u*2
#define rc u*2+1
using namespace std;
const int N=1e5+50;
int n,m,a[N];
struct node{
int l0,l1,r0,r1,sum0,sum1,mx0,mx1;
}tr[8*N];
int tag[8*N],retag[8*N];
void hb(int u,int l,int r){
tr[u].sum0=tr[l].sum0+tr[r].sum0;
tr[u].sum1=tr[l].sum1+tr[r].sum1;
tr[u].l1 = tr[l].sum0==0 ? tr[l].sum1+tr[r].l1 : tr[l].l1;
tr[u].l0 = tr[l].sum1==0 ? tr[l].sum0+tr[r].l0 : tr[l].l0;
tr[u].r1 = tr[r].sum0==0 ? tr[r].sum1+tr[l].r1 : tr[r].r1;
tr[u].r0 = tr[r].sum1==0 ? tr[r].sum0+tr[l].r0 : tr[r].r0;
tr[u].mx0=max(max(tr[l].mx0,tr[r].mx0),tr[l].r0+tr[r].l0);
tr[u].mx1=max(max(tr[l].mx1,tr[r].mx1),tr[l].r1+tr[r].l1);
}
void pushdown(int u,int l,int r){
if(tag[u]!=-1){
int mid=(l+r)>>1;
tr[lc].sum0=tag[u]==0 ? mid-l+1: 0; tr[lc].sum1=tag[u]==1 ? mid-l+1: 0;
tr[lc].l0= tag[u]==0 ? mid-l+1 : 0; tr[lc].l1 =tag[u]==1 ? mid-l+1 : 0;
tr[lc].r0= tag[u]==0 ? mid-l+1 : 0; tr[lc].r1 =tag[u]==1 ? mid-l+1 : 0;
tr[lc].mx0=tag[u]==0 ? mid-l+1 : 0; tr[lc].mx1=tag[u]==1 ? mid-l+1 : 0;
tr[rc].sum0=tag[u]==0 ? r-mid: 0; tr[rc].sum1=tag[u]==1 ? r-mid: 0;
tr[rc].l0= tag[u]==0 ? r-mid : 0; tr[rc].l1 =tag[u]==1 ? r-mid : 0;
tr[rc].r0= tag[u]==0 ? r-mid : 0; tr[rc].r1 =tag[u]==1 ? r-mid : 0;
tr[rc].mx0=tag[u]==0 ? r-mid : 0; tr[rc].mx1=tag[u]==1 ? r-mid : 0;
tag[lc]=tag[u];
tag[rc]=tag[u];
retag[lc]=0;
retag[rc]=0;
}
if(retag[u]==1){
swap(tr[lc].sum0,tr[lc].sum1);
swap(tr[lc].l1,tr[lc].l0);
swap(tr[lc].r1,tr[lc].r0);
swap(tr[lc].mx1,tr[lc].mx0);
swap(tr[rc].sum0,tr[rc].sum1);
swap(tr[rc].l1,tr[rc].l0);
swap(tr[rc].r1,tr[rc].r0);
swap(tr[rc].mx1,tr[rc].mx0);
retag[lc]^=1;
retag[rc]^=1;
}
tag[u]=-1;retag[u]=0;
}
void build(int u,int l,int r){
tag[u]=-1;
if(l==r){
tr[u].l1=a[l]==1 ? 1 : 0;
tr[u].r1=a[l]==1 ? 1 : 0;
tr[u].sum1=a[l]==1 ? 1 : 0;
tr[u].mx1=a[l]==1 ? 1 : 0;
tr[u].l0=a[l]==0 ? 1 : 0;
tr[u].r0=a[l]==0 ? 1 : 0;
tr[u].sum0=a[l]==0 ? 1 : 0;
tr[u].mx0=a[l]==0 ? 1 : 0;
return;
}
int mid=(l+r)>>1;
build(lc,l,mid);
build(rc,mid+1,r);
hb(u,lc,rc);
return;
}
void modify(int u,int l,int r,int x,int y,int k){
if(r<x || l>y) return ;
if(l>=x && r<=y){
tr[u].sum1 = (k==1 ? r-l+1 : 0);
tr[u].l1= (k==1 ? r-l+1 : 0);
tr[u].r1= (k==1 ? r-l+1 : 0);
tr[u].mx1=(k==1 ? r-l+1 : 0);
tr[u].sum0 = (k==0 ? r-l+1 : 0);
tr[u].l0= (k==0 ? r-l+1 : 0);
tr[u].r0= (k==0 ? r-l+1 : 0);
tr[u].mx0=(k==0 ? r-l+1 : 0);
retag[u]=0;
tag[u]=k;
return;
}
pushdown(u,l,r);
int mid=(l+r)>>1;
modify(lc,l,mid,x,y,k);
modify(rc,mid+1,r,x,y,k);
hb(u,lc,rc);
return;
}
void remodify(int u,int l,int r,int x,int y){
if(r<x || l>y) return ;
if(l>=x && r<=y){
swap(tr[u].sum0,tr[u].sum1);
swap(tr[u].l1,tr[u].l0);
swap(tr[u].r1,tr[u].r0);
swap(tr[u].mx1,tr[u].mx0);
retag[u]^=1;
return ;
}
pushdown(u,l,r);
int mid=(l+r)>>1;
remodify(lc,l,mid,x,y);
remodify(rc,mid+1,r,x,y);
hb(u,lc,rc);
return;
}
int ask1(int u,int l,int r,int x,int y){
if(r<x || l>y) return 0;
if(l>=x && r<=y){
return tr[u].sum1;
}
pushdown(u,l,r);
int mid=(l+r)>>1;
return ask1(lc,l,mid,x,y)+ask1(rc,mid+1,r,x,y);
}
int ask2(int u,int l,int r,int x,int y){
if(r<x || l>y) return -1e9;
if(l>=x && r<=y){
return tr[u].mx1;
}
pushdown(u,l,r);
int mid=(l+r)>>1;
return max(max(ask2(lc,l,mid,x,y),ask2(rc,mid+1,r,x,y)),min(tr[lc].r1,mid-x+1)+min(tr[rc].l1,y-mid));
}
int main(){
ios::sync_with_stdio(0);
cin.tie(0);cout.tie(0);
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>a[i];
}
for(int i=1;i<=4*n;i++){
tag[i]=-1;
}
build(1,1,n);
for(int i=1;i<=m;i++){
int op,x,y;
cin>>op>>x>>y;
x++;y++;
if(op==0){
modify(1,1,n,x,y,0);
}else if(op==1){
modify(1,1,n,x,y,1);
}else if(op==2){
remodify(1,1,n,x,y);
}
else if(op==3){
cout<<ask1(1,1,n,x,y)<<'\n';
}else if(op==4){
cout<<ask2(1,1,n,x,y)<<'\n';
}
}
return 0;
}
8.数据结构杂题
喜欢写一天 DS 中的大模拟吗?反正我不喜欢。
观察题目,我们先发现如果从左至右考虑项链,相同种类的珠子只有靠右侧的在未来的作用较大,所以我们仅保留最右侧的珠子。自然想到将答案离线,并以右端点排序。需要动态维护区间和,想到树状数组。
AC code:
#include<bits/stdc++.h>
#define int long long
using namespace std;
int lowbit(int p){return p&-p;}
const int maxn=1e6+50;
int n,m;
struct node{
int a,id;
}s[maxn];
int t[maxn];
bool vis[maxn];
int last[maxn];
int ask[maxn];
int ans[maxn];
void add(int p,int x){
while(p<=n){
t[p]+=x;
p+=lowbit(p);
}
return;
}
long long count(int p){
long long res=0;
while(p){
res+=t[p];
p-=lowbit(p);
}
return res;
}
struct ASK{
int l,r;
int id;
}q[maxn];
bool cmp(ASK a,ASK b){
return a.r<b.r;
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0);cout.tie(0);
cin>>n;
for(int i=1;i<=n;i++){
cin>>s[i].a;
s[i].id=i;
}
cin>>m;
for(int i=1;i<=m;i++){
cin>>q[i].l>>q[i].r;
q[i].id=i;
}
sort(q+1,q+1+m,cmp);
int cnt=1;
for(int i=1;i<=n;i++){
if(vis[s[i].a]==0){
vis[s[i].a]=1;
last[s[i].a]=i;
add(i,1);
}else if(vis[s[i].a]==1){
if(last[s[i].a]!=0){
add(last[s[i].a],-1);
add(i,1);
}
last[s[i].a]=i;
}
while(q[cnt].r==i){
ans[q[cnt].id]=count(q[cnt].r)-count(q[cnt].l-1);
cnt++;
}
if(cnt==m+1) break;
}
for(int i=1;i<=m;i++){
cout<<ans[i]<<'\n';
}
return 0;
}
想对于每一个点建权值线段树维护最大值,只能动态开点,利用树上差分,统计子数和时只能合并线段树。
说实话感觉这题并不适合做线段树合并板子
AC code:
#include <iostream>
#include <math.h>
#define N 4000010
#define INF 200010
using namespace std;
struct node{
int maxx,lc,rc,id;
}tree[4*N];int tot;
int root[4*N];
struct NODE{
int to,nxt;
}edge[4*N];int n,m;
int father[4*N],head[4*N],dep[4*N],f[4*N][20];
void add(int u,int v){
edge[++tot].to=v;
edge[tot].nxt=head[u];
head[u]=tot;
}
//void pushdown(int u){
// if(!tree[u].lc) tree[u].lc=++tot;
// if(!tree[u].rc) tree[u].rc=++tot;
//}
void pushup(int u){
int lc=tree[u].lc,rc=tree[u].rc;
if(tree[lc].maxx>=tree[rc].maxx){
tree[u].maxx=tree[lc].maxx;
tree[u].id=tree[lc].id;
}
else{
tree[u].maxx=tree[rc].maxx;
tree[u].id=tree[rc].id;
}
//cout<<"pu"<<" "<<u<<" "<<tree[u].id<<'\n';
}
int modify(int u,int l,int r,int x,int k){
if(!u) u=++tot;
if(l==r){
tree[u].maxx+=k;
tree[u].id=l;
return u;
}
int mid=(l+r)>>1;
if(x<=mid) tree[u].lc=modify(tree[u].lc,l,mid,x,k);
else tree[u].rc=modify(tree[u].rc,mid+1,r,x,k);
pushup(u);
return u;
}
int lca(int x,int y){
if(dep[x]>dep[y]) swap(x,y);
int k=log2(dep[y])+1;
for(int i=k;i>=0;i--) if(dep[y]-(1<<i)>=dep[x]) y=f[y][i];
if(x==y) return x;
for(int i=k;i>=0;i--){
if(f[x][i]!=f[y][i]&&f[x][i]!=0){
x=f[x][i],y=f[y][i];
}
}
return f[x][0];
}
void dfs(int u,int fa){
dep[u]=dep[fa]+1;
for(int i=head[u];i;i=edge[i].nxt){
int v=edge[i].to;
if(v!=fa){
father[v]=u;
f[v][0]=u;
dfs(v,u);
}
}
}
void prepare(){
for(int j=1;(1<<j)<=n;j++){
for(int i=1;i<=n;i++){
if(f[i][j-1]!=0) f[i][j]=f[f[i][j-1]][j-1];
}
}
}
void unify(int p1,int p2,int l,int r){
if(l==r){
tree[p1].maxx+=tree[p2].maxx;
return;
}
int mid=(l+r)/2;
if(tree[p1].lc&&tree[p2].lc) unify(tree[p1].lc,tree[p2].lc,l,mid);
else if(tree[p2].lc) tree[p1].lc=tree[p2].lc;
if(tree[p1].rc&&tree[p2].rc) unify(tree[p1].rc,tree[p2].rc,mid+1,r);
else if(tree[p2].rc) tree[p1].rc=tree[p2].rc;
pushup(p1);
}
void dfss(int u,int fa){
for(int i=head[u];i;i=edge[i].nxt){
int v=edge[i].to;
if(v==fa) continue;
dfss(v,u);
unify(root[u],root[v],1,INF);
}
}
int main(){
cin>>n>>m;
for(int i=1;i<=n-1;i++){
int a,b;
cin>>a>>b;
add(a,b);
add(b,a);
}
tot=0;
dfs(1,0);
prepare();
for(int i=1;i<=m;i++){
int x,y,z;
cin>>x>>y>>z;
int rt=lca(x,y);
root[x]=modify(root[x],1,INF,z,1);
root[y]=modify(root[y],1,INF,z,1);
root[rt]=modify(root[rt],1,INF,z,-1);
if(father[rt]!=0)
root[father[rt]]=modify(root[father[rt]],1,INF,z,-1);
}
dfss(1,0);
for(int i=1;i<=n;i++){
cout<<((tree[root[i]].maxx == 0)?0:(tree[root[i]].id))<<'\n';
}
return 0;
}
[HNOI2012] 永无乡
动态开点权值线段树维护重要度+线段树上二分查询第k大值编号,对于建桥的情况直接合并线段树即可。
//动态开点线段树维护重要度+线段树上二分查询第k大值编号
#include<iostream>
#include<queue>
#include<algorithm>
#include<cstring>
#include<vector>
using namespace std;
const int N=1e5+50;
const int M=3e5+50;
struct Tree{
int lc,rc;
int l,r;
int sum;
}tr[100*N];
int tot,idx[N];
int n,m,q,a,fa[N],rt[N];
int find(int x){
if(x==fa[x]) return x;
return fa[x]=find(fa[x]);
}
int modify(int l,int r,int p){
int u=++tot;
tr[u].sum=1;
tr[u].l=l;
tr[u].r=r;
if(l==r){return u;}
int mid=(l+r)>>1;
if(p<=mid) tr[u].lc=modify(l,mid,p);
else tr[u].rc=modify(mid+1,r,p);
return u;
}
int merge(int u1,int u2,int l,int r){
if(!u1 && !u2) return 0;
if(!u1) return u2;
if(!u2) return u1;
//暴力新开点合并
int u=++tot;
tr[u].l=l;tr[u].r=r;
tr[u].sum=tr[u1].sum+tr[u2].sum;
int mid=(l+r)>>1;
tr[u].lc=merge(tr[u1].lc,tr[u2].lc,l,mid);
tr[u].rc=merge(tr[u1].rc,tr[u2].rc,mid+1,r);
return u;
}
//线段树上二分查找
int query(int u,int k){
if(tr[u].sum<k) return -1;
if(tr[u].l==tr[u].r) return idx[tr[u].l];
if(tr[tr[u].lc].sum>=k) return query(tr[u].lc,k);
else return query(tr[u].rc,k-tr[tr[u].lc].sum);
}
int main(){
ios::sync_with_stdio(0);
cin.tie(0);cout.tie(0);
cin>>n>>m;
for(int i=1;i<=n;i++){
fa[i]=i;
cin>>a;
rt[i]=modify(1,n,a);
idx[a]=i;
}
for(int i=1;i<=m;i++){
int u,v;
cin>>u>>v;
int aa=find(u);
int bb=find(v);
if(aa==bb) continue;
fa[aa]=bb;
rt[bb]=merge(rt[aa],rt[bb],1,n);
}
cin>>q;
for(int i=1;i<=q;i++){
char op;
int x,y;
cin>>op>>x>>y;
if(op=='Q'){
cout<<query(rt[find(x)],y)<<'\n';
}else if(op=='B'){
int aa=find(x);
int bb=find(y);
if(aa==bb) continue;
fa[aa]=bb;
rt[bb]=merge(rt[aa],rt[bb],1,n);
}
}
return 0;
}
9.树链剖分/重链剖分
树剖好啊,无脑啊。
树剖需要 \(O(2\times n)\) 的预处理,就是两个 DFS 统计信息。
第一个 DFS 统计每个节点的深度,以该节点为根的子树的大小,以及每个结点的重子树节点,每个节点的父亲共四个信息。
第二个 DFS 统计每条链的顶点,以及 DFS 序,有点权时还需要把点权按 DFS 序排列。
最后得到一个序列,可以用线段树维护。
两遍 DFS 的代码如下:
int siz[N],fa[N],top[N],dep[N],dis[N],hson[N],dfn[N],dfx,dist[N];
void dfs1(int u,int f){
dep[u]=dep[f]+1;
siz[u]=1;
fa[u]=f;
for(int i=head[u];i;i=e[i].nxt){
int v=e[i].to;
if(v==f) continue;
dfs1(v,u);
siz[u]+=siz[v];
if(siz[v]>siz[hson[u]]){
hson[u]=v;
}
}
return ;
}
void dfs2(int u,int tp){
top[u]=tp;
dfn[u]=++dfx;
dist[dfx]=dis[u];
if(hson[u]) dfs2(hson[u],tp);
else return;
for(int i=head[u];i;i=e[i].nxt){
int v=e[i].to;
if(v==fa[u] || v==hson[u]) continue;
dfs2(v,v);
}
return ;
}
然后对于一些操作就转化成了线段树的操作。
问题是两个点之间的路径与线段树上的区间的关系怎么转化。
由于 DFS 序的特性,\(x\) 后 \(siz_x\) 个节点一定为 \(x\) 的子树,可以用来处理对子树操作的区间了。
由于我们是重链剖分,所以一条链上的节点一定是连续的。那么一条链上的点到链的顶点的区间就可以表示为:\(dfn_{top_x}->dfn_x\)
那么如果不在一条链上呢?我们可以先处理点所在链的顶点,顶点深度较大的点,让它处理完链之后,跳到链的顶点的父亲,最后让两个点跳到一条链上。
具体代码如下,包含对子树操作和对两个间的路径操作:
void change(int x,int y,int k){
k%=p;
while(top[x]!=top[y]){
if(dep[top[x]]<dep[top[y]]) swap(x,y);
modify(1,1,n,dfn[top[x]],dfn[x],k);
x=fa[top[x]];
}
if(dep[x]>dep[y]) swap(x,y);
modify(1,1,n,dfn[x],dfn[y],k);
return ;
}
void changeson(int x,int k){
modify(1,1,n,dfn[x],dfn[x]+siz[x]-1,k);
}
int ask1(int x,int y){
int res=0;
while(top[x]!=top[y]){
if(dep[top[x]]<dep[top[y]]) swap(x,y);
res+=query(1,1,n,dfn[top[x]],dfn[x])%p;res%=p;
x=fa[top[x]];
}
if(dep[x]>dep[y]) swap(x,y);
res+=query(1,1,n,dfn[x],dfn[y])%p;
return res%p;
}
int askson(int x){
return query(1,1,n,dfn[x],dfn[x]+siz[x]-1)%p;
}
把上述两份代码拼起来再写个线段树,就能得到 模板 的代码。
接下来看两道题:
软件包管理器
手玩样例后发现删除就是统计子树和之后将子树修改为 \(0\)。
安装就是先统计 \(res1\) 为 \(x\) 节点到根上的路径和,再将路径修改为 \(1\) ,最后 \(res2\) 为新的路径和,\(ans=res2-res1\)。
直接做即可,注意下标从 \(0\) 开始:
#include<iostream>
#include<cstring>
#define lc u*2
#define rc u*2+1
using namespace std;
const int N=1e5+50;
struct Edge{
int to,nxt;
}e[2*N];
int head[2*N],tot;
void add(int u,int v){
e[++tot].to=v;
e[tot].nxt=head[u];
head[u]=tot;
return ;
}
int n,q;
int fa[N],dep[N],siz[N],hson[N],dfn[N],dfx,top[N];
int tr[4*N],tag[4*N];
void pushup(int u){
tr[u]=tr[lc]+tr[rc];
return ;
}
void pushdown(int u,int l,int r){
if(tag[u]==-1) return ;
int mid=(l+r)>>1;
tr[lc]=tag[u]*(mid-l+1);
tr[rc]=tag[u]*(r-mid);
tag[lc]=tag[u];
tag[rc]=tag[u];
tag[u]=-1;
}
void modify(int u,int l,int r,int x,int y,int k){
if(l>=x && r<=y){
tr[u]=k*(r-l+1);
tag[u]=k;
return ;
}
pushdown(u,l,r);
int mid=(l+r)>>1;
if(x<=mid) modify(lc,l,mid,x,y,k);
if(mid<y) modify(rc,mid+1,r,x,y,k);
pushup(u);
return ;
}
int query(int u,int l,int r,int x,int y){
if(l>=x && r<=y){
return tr[u];
}
pushdown(u,l,r);
int res=0;
int mid=(l+r)>>1;
if(x<=mid) res+=query(lc,l,mid,x,y);
if(mid<y) res+=query(rc,mid+1,r,x,y);
return res;
}
void dfs1(int u,int f){
dep[u]=dep[f]+1;
siz[u]=1;
fa[u]=f;
for(int i=head[u];i;i=e[i].nxt){
int v=e[i].to;
if(v==f) continue;
dfs1(v,u);
siz[u]+=siz[v];
if(siz[v]>siz[hson[u]]){
hson[u]=v;
}
}
return ;
}
void dfs2(int u,int tp){
top[u]=tp;
dfn[u]=++dfx;
if(hson[u]) dfs2(hson[u],tp);
for(int i=head[u];i;i=e[i].nxt){
int v=e[i].to;
if(v==fa[u] || v==hson[u]) continue;
dfs2(v,v);
}
return ;
}
int unins(int x){
int res=query(1,1,n,dfn[x],dfn[x]+siz[x]-1);
modify(1,1,n,dfn[x],dfn[x]+siz[x]-1,0);
return res;
}
int queryin(int x){
int res=0;
while(top[1]!=top[x]){
res+=query(1,1,n,dfn[top[x]],dfn[x]);
x=fa[top[x]];
}
res+=query(1,1,n,dfn[1],dfn[x]);
return res;
}
void modifyin(int x){
while(top[1]!=top[x]){
modify(1,1,n,dfn[top[x]],dfn[x],1);
x=fa[top[x]];
}
modify(1,1,n,dfn[1],dfn[x],1);
return ;
}
int ins(int x){
int old=queryin(x);
modifyin(x);
int ans=queryin(x)-old;
return ans;
}
int main(){
ios::sync_with_stdio(0);
cin.tie(0);cout.tie(0);
cin>>n;
for(int i=1;i<=4*n;i++){
tag[i]=-1;
}
for(int i=2;i<=n;i++){
int v;
cin>>v;
v++;
add(i,v);
add(v,i);
}
dfs1(1,1);
dfs2(1,1);
cin>>q;
for(int i=1;i<=q;i++){
string op;
int x;
cin>>op>>x;
x++;
if(op=="install"){
cout<<ins(x)<<'\n';
}else if(op=="uninstall"){
cout<<unins(x)<<'\n';
}
}
return 0;
}
旅行
仔细阅读后发现对每个宗教开一棵线段树,需要动态开点。
然后直接做即可,具体的一些细节:走城市时需要记录最开始的根,防止走着走着信的宗教变了。
代码如下:
#include<iostream>
#include<queue>
#include<algorithm>
#include<cstring>
#include<vector>
using namespace std;
const int N=1e5+50;
int n,q,tot;
struct Tree{
int lc,rc;
int sum,mx;
}tr[16*N];
struct City{
int w,c;
}city[N];
vector<int> vec[N];
string op;
int wt[N],ct[N],dfn[N],siz[N],top[N],dep[N],fa[N],hson[N],cnt;
int rt[N];
void dfs1(int u,int father){
fa[u]=father;
dep[u]=dep[father]+1;
siz[u]=1;
for(int i=0;i<vec[u].size();i++){
int v=vec[u][i];
if(v==father) continue;
dfs1(v,u);
siz[u]+=siz[v];
if(siz[v]>siz[hson[u]]){
hson[u]=v;
}
}
return ;
}
void dfs2(int u,int tp){
dfn[u]=++cnt;
top[u]=tp;
wt[cnt]=city[u].w;
ct[cnt]=city[u].c;
if(!hson[u]) return ;
dfs2(hson[u],tp);
for(int i=0;i<vec[u].size();i++){
int v=vec[u][i];
if(v==fa[u] || v==hson[u]) continue;
dfs2(v,v);
}
return ;
}
void pushup(int u){
tr[u].sum=tr[tr[u].lc].sum+tr[tr[u].rc].sum;
tr[u].mx=max(tr[tr[u].lc].mx,tr[tr[u].rc].mx);
return ;
}
int modify(int u,int l,int r,int x,int w){
if(!u) u=++tot;
if(l==r){
tr[u].sum=w;
tr[u].mx=w;
return u;
}
int mid=(l+r)>>1;
if(mid>=x){
tr[u].lc=modify(tr[u].lc,l,mid,x,w);
}
if(mid<x){
tr[u].rc=modify(tr[u].rc,mid+1,r,x,w);
}
pushup(u);
return u;
}
int query(int u,int l,int r,int x,int y){
int res=0;
if(!u) return 0;
if(l>=x && r<=y){
res+=tr[u].sum;
return res;
}
int mid=(l+r)>>1;
if(x<=mid){
res+=query(tr[u].lc,l,mid,x,y);
}
if(mid<y){
res+=query(tr[u].rc,mid+1,r,x,y);
}
return res;
}
int qmax(int u,int l,int r,int x,int y){
int res=0;
if(!u) return 0;
if(l>=x && r<=y){
res=max(res,tr[u].mx);
return res;
}
int mid=(l+r)>>1;
if(x<=mid){
res=max(res,qmax(tr[u].lc,l,mid,x,y));
}
if(mid<y){
res=max(res,qmax(tr[u].rc,mid+1,r,x,y));
}
return res;
}
int queryh(int x,int y){
int ans=0,r=rt[city[x].c];
while(top[x]!=top[y]){
if(dep[top[x]]<dep[top[y]]) swap(x,y);
int res=query(r,1,n,dfn[top[x]],dfn[x]);
ans+=res;
x=fa[top[x]];
}
if(dep[x]>dep[y]) swap(x,y);
ans+=query(r,1,n,dfn[x],dfn[y]);
return ans;
}
int maxh(int x,int y){
int ans=0,r=rt[city[x].c];
while(top[x]!=top[y]){
if(dep[top[x]]<dep[top[y]]) swap(x,y);
int res=qmax(r,1,n,dfn[top[x]],dfn[x]);
ans=max(ans,res);
x=fa[top[x]];
}
if(dep[x]>dep[y]) swap(x,y);
ans=max(ans,qmax(r,1,n,dfn[x],dfn[y]));
return ans;
}
int main(){
ios::sync_with_stdio(0);
cin.tie(0);cout.tie(0);
cin>>n>>q;
for(int i=1;i<=n;i++){
cin>>city[i].w>>city[i].c;
}
for(int i=1;i<n;i++){
int u,v;
cin>>u>>v;
vec[u].push_back(v);
vec[v].push_back(u);
}
dfs1(1,0);
dfs2(1,1);
for(int i=1;i<=n;i++){
rt[city[i].c]=modify(rt[city[i].c],1,n,dfn[i],city[i].w);
}
for(int i=1;i<=q;i++){
cin>>op;
if(op=="CC"){
int x,c;
cin>>x>>c;
modify(rt[city[x].c],1,n,dfn[x],0);
city[x].c=c;
modify(rt[c],1,n,dfn[x],city[x].w);
}else if(op=="CW"){
int x,w;
cin>>x>>w;
city[x].w=w;
modify(rt[city[x].c],1,n,dfn[x],w);
}else if(op=="QS"){
int x,y;
cin>>x>>y;
cout<<queryh(x,y)<<'\n';
}else if(op=="QM"){
int x,y;
cin>>x>>y;
cout<<maxh(x,y)<<'\n';
}
}
return 0;
}
尾声
寒假集训不知不觉要结束了,希望我们都有令自己满意的收获!

浙公网安备 33010602011771号