dp&图论 杂题选做

Posted on 2024-03-02 17:14  _XOFqwq  阅读(7)  评论(0编辑  收藏  举报

开个新坑 qwq。

upd:CSP 前一周暂时停更。

upd:暂时不会更了。


P1099

经典套路题。

算法一:枚举。

先 dfs 求出树的直径,再枚举直径上的每条路径,再 dfs 一遍求出最小偏心距即可。

时间复杂度 \(O(n^3)\),足以通过本题(由此可见本题有多水)。

算法二:双指针。

通过观察可以发现,在固定左端点的情况下,路径的右端点不断延伸,偏心距不会变大。

所以我们仅需枚举左端点,运用双指针算法找到路径长度 \(\le s\) 且最远的右端点,从而减少候选路径的数量。

其他的与算法一是相同的。时间复杂度 \(O(n^2)\)

算法三:双指针+前缀和。

如图(加粗的点组成的路径为直径):

令直径上的点为 \(a_1,a_2,...,a_k\)\(P(i,j)\) 表示直径上的路径 \((i,j)\)\(d_i\) 表示直径外一点到直径上一点 \(a_i\) 的最远距离。

我们假定点 \(p\) 是距离直径最远的点,则可得在这条直径上选择的路径 \((i,j)\) 的最小偏心距为

\[\max\{\max_{i<p<j} d_{a_p},P(a_1,a_i),P(a_j,a_k)\} \]

这个式子的后两项均可使用前缀和维护,重点考虑第一项如何维护。

我们有这样一个定理:

  • 对于 \(1 \le l \le i\),有 \(d_{a_l} \le P(a_1,a_i)\);同理对于 \(j \le l \le k\),有 \(d_{a_l} \le P(a_j,a_k)\)

证明:根据直径是树中最长的简单路径,可得 \(d_{a_l}+P(a_i,a_l)<P(a_1,a_i)\),又根据 \(P(a_i,a_l)>0\),因此原命题得证。

这个定理说明了 \(p\) 取在 \(1 \sim i\) 中或 \(j \sim k\) 中时,对于答案没有贡献。

因此我们可以扩大 \(p\) 的取值范围,从 \(i<p<j\) 变为 \(1 \le p \le k\),于是偏心距的计算公式变为

\[\max\{\max_{1 \le p \le k} d_{a_p},P(a_1,a_i),P(a_j,a_k)\} \]

其中 \(\max_{1 \le p \le k} d_{a_p}\) 显然是一个定值,因此无需维护第一项。

时间复杂度降至 \(O(n)\)

代码:

//省略快读快写
#include<bits/stdc++.h>
using namespace std;

int n,s,c,tot;
int dia[300031],f[300031];
int dep[300031],pre[300031],post[300031];
bool vis[300031];
struct EdgeInfo{
	int to,w;
};
vector<EdgeInfo> G[300031];

void dfs(int u,int fa){
	f[u]=fa;
	for(auto i:G[u]){
		if(i.to==fa||vis[i.to]) continue;
		dep[i.to]=dep[u]+i.w;
		if(dep[i.to]>dep[c]) c=i.to;
		dfs(i.to,u);
	}
}
void get_diameter(){
	dfs(1,0);
	dep[c]=0;
	dfs(c,0);
	for(int i=c;i;i=f[i])
		dia[++tot]=i,pre[tot]=dep[i];
	reverse(dia+1,dia+tot+1),reverse(pre+1,pre+tot+1);
	for(int i=tot;i>=1;i--) post[i]=pre[tot]-pre[i];
}
void solve(){
	for(int i=1;i<=tot;i++) vis[dia[i]]=1;
	int maxd=-1e9;
	for(int i=1;i<=tot;i++){
		dep[dia[i]]=0,c=0;
		dfs(dia[i],0);
		maxd=max(maxd,dep[c]);
	}
	int min_ecc=1e9;
	for(int l=1,r=1;l<=tot;l++){
		while(r<=tot&&pre[r+1]-pre[l]<=s) r++;
		min_ecc=min(min_ecc,max(max(pre[l],post[r]),maxd));
	}
	print(min_ecc);
}

int main(){
	n=rd(),s=rd();
	for(int i=1,u,v,w;i<n;i++){
		u=rd(),v=rd(),w=rd();
		G[u].push_back({v,w});
		G[v].push_back({u,w});
	}
	get_diameter();
	solve();
	return 0;
}

P3128

引理:

  • 两节点 \(u,v\)\(\operatorname{lca}\) 一定在 \(u,v\) 间的最短路径上。

由此引理,我们便能分别对于 \(u,\operatorname{lca}(u,v)\)\(\operatorname{lca}(u,v),v\) 间的最短路径进行树上差分。

要将 \(u,v\) 的最短路径上的节点都 \(+1\),则 \(d_u\)\(d_v\)\(d\) 是差分数组)都 \(+1\),并且将重复计算的 \(d_{\operatorname{lca}(u,v)}-2\)

这样操作 \(d_{\operatorname{lca}(u,v)}\) 并没有被加上 \(1\),所以 \(d_{\operatorname{lca}(u,v)}\) 只应该减 \(1\)

但只减 \(1\) 就会导致 \(d_{\operatorname{lca}(u,v)}\) 的父亲也会被影响,于是 \(d_{\operatorname{lca}(u,v)}\) 的父亲也应该减 \(1\)

最后对于整棵树求一边前缀和即可。

#include<bits/stdc++.h>
using namespace std;

int n,k,ans=-1e9;
int dep[500031],fa[500031][31];
vector<int> G[500031];
int vis[500031];
int g[500031];

void dfs(int now){
    int len=G[now].size();
    vis[now]=1;
    for(int i=0;i<len;i++){
        int nxt=G[now][i];
        if(!vis[nxt]){
            dep[nxt]=dep[now]+1;
            fa[nxt][0]=now;
            dfs(nxt);
        }
    }
}
void st(int n){
    for(int j=1;(1<<j)<=n;j++)
        for(int i=1;i<=n;i++)
            fa[i][j]=fa[fa[i][j-1]][j-1];
}
int LCA(int u,int v){
    if(dep[u]<dep[v]) swap(u,v);
    int h=dep[u]-dep[v];
    for(int i=0;i<20;i++)
		if(h&(1<<i)) u=fa[u][i];
    if(u==v) return u;
    for(int i=19;i>=0;i--)
        if(fa[u][i]!=fa[v][i])
			u=fa[u][i],v=fa[v][i];
    return fa[u][0];
}
void getans(int u,int f){
	for(auto i:G[u]){
		if(i==f) continue;
		getans(i,u);
		g[u]+=g[i];
	}
	ans=max(ans,g[u]);
}


int main(){
	cin>>n>>k;
	for(int i=1,u,v;i<n;i++){
		cin>>u>>v;
		G[u].push_back(v),G[v].push_back(u);
	}
	dfs(1),st(n);
	for(int i=1,s,t;i<=k;i++){
		cin>>s>>t;
		int lca=LCA(s,t);
		g[s]++,g[t]++,g[lca]--,g[fa[lca][0]]--;
	}
	getans(1,0);
	cout<<ans;
	return 0;
}

P1725

一眼 dp。

朴素地 dp 很好想,直接对于每一个 \(i\),在 \([i-L,i-R]\) 这个区间中寻找最优的前置状态转移即可。时间复杂度 \(O(n^2)\),无法接受。

通过观察可以发现,\([i-L,i-R]\) 这个区间,是会随着 \(i\) 往后移动而跟着移动的。

形式化的,若 \(i \gets i+1\),则相应的前置状态区间 \([i-L,i-R] \gets [i-L+1,i-R+1]\)

显然可以使用滑动窗口维护这样的区间,转移复杂度成功降至 \(O(1)\),总时间复杂度 \(O(n)\)

#include<bits/stdc++.h>
using namespace std;

int n,ans=-1e9,l,r;
int dp[200031];
int a[200031],q[200031];
int head=0,tail=0;

void insert(int x){
	while(head<=tail&&dp[x]>=dp[q[tail]]) tail--;
	q[++tail]=x;
}
int getmax(int x){
	while(q[head]+r<x) head++;
	return q[head];
}

int main(){
	cin>>n>>l>>r;
	for(int i=0;i<=n;i++) cin>>a[i];
 	memset(dp,128,sizeof(dp));
	dp[0]=0;
	for(int i=l;i<=n;i++){
		insert(i-l);
		int from=getmax(i);
		dp[i]=dp[from]+a[i];
		if(i+r>n) ans=max(ans,dp[i]);
	}
	cout<<ans;
	return 0;
}

P3384

写了 \(114\) 行的树链剖分模板题(喜)

首先进行两遍 dfs:

第一遍求出每个节点的深度、子树大小、父节点和重儿子;

第二遍求出每个节点的每个节点的新编号、新权值和每条重链的顶部。

做完这两遍 dfs 之后,就可以维护线段树回答询问了。

对于 \(1\) 操作,让 \(x,y\) 中深度更深的节点不停按照重链向上跳,其间不断令链顶与 \(x\) 进行区间加,最后当 \(x\) 的深度 \(\ge y\) 时,对 \(x,y\) 进行区间加即可。

对于 \(2\) 操作,其与 \(1\) 操作同理,只是将区间加换成了区间求和。

对于 \(3\) 操作,因为树链剖分之后的新编号都是连续的,所以直接对于 \(x,x+sz_x-1\) 进行区间加即可。

对于 \(4\) 操作,其与 \(3\) 操作同理,也只是将区间加换成了区间求和。

然后这题就做完了,时间复杂度 \(O(n \log ^2 n)\)

#include<bits/stdc++.h>
#define int long long
using namespace std;

int n,m,r,mod,tot;
int w[400031],ww[400031];
vector<int> G[400031];
int dep[400031],fa[400031],sz[400031],son[400031],top[400031],id[400031];
struct SegmentTree{
    int l,r,sum,add;
}t[800031];

void bld(int p,int l,int r){
    t[p].l=l; t[p].r=r;
    if(l==r){ t[p].sum=ww[l]%mod; return; }
    int mid=(l+r)/2;
    bld(p*2,l,mid);
    bld(p*2+1,mid+1,r);
    t[p].sum=(t[p*2].sum+t[p*2+1].sum)%mod;
}
void spd(int p){
    if(t[p].add){
        t[p*2].sum+=t[p].add*(t[p*2].r-t[p*2].l+1);
        t[p*2+1].sum+=t[p].add*(t[p*2+1].r-t[p*2+1].l+1);
        t[p*2].sum%=mod,t[p*2+1].sum%=mod;
        t[p*2].add+=t[p].add,t[p*2].add%=mod;
        t[p*2+1].add+=t[p].add,t[p*2+1].add%=mod;
        t[p].add=0;
    }
}
void upd(int p,int l,int r,int d){
    if(l<=t[p].l&&r>=t[p].r){
        t[p].sum+=(int)d*(t[p].r-t[p].l+1),t[p].sum%=mod;
        t[p].add+=d,t[p].add%=mod;
        return;
    }
    spd(p);
    int mid=(t[p].l+t[p].r)/2;
    if(l<=mid) upd(p*2,l,r,d);
    if(r>mid) upd(p*2+1,l,r,d);
    t[p].sum=(t[p*2].sum+t[p*2+1].sum)%mod;
}
int qry(int p,int l,int r){
    if(l<=t[p].l&&r>=t[p].r) return t[p].sum%mod;
    spd(p);
    int val=0;
    int mid=(t[p].l+t[p].r)/2;
    if(l<=mid) val+=qry(p*2,l,r),val%=mod;
    if(r>mid) val+=qry(p*2+1,l,r),val%=mod;
	return val%mod;
}
void updRange(int x,int y,int z){
	z%=mod;
	while(top[x]!=top[y]){
		if(dep[top[x]]<dep[top[y]]) swap(x,y);
		upd(1,id[top[x]],id[x],z),x=fa[top[x]];
	}
	if(dep[x]>dep[y]) swap(x,y);
	upd(1,id[x],id[y],z);
}
int qryRange(int x,int y){
	int ans=0;
	while(top[x]!=top[y]){
		if(dep[top[x]]<dep[top[y]]) swap(x,y);
		ans+=qry(1,id[top[x]],id[x]),ans%=mod,x=fa[top[x]];
	}
	if(dep[x]>dep[y]) swap(x,y);
	ans+=qry(1,id[x],id[y]),ans%=mod;
	return ans;
}
void updSon(int x,int y){
	upd(1,id[x],id[x]+sz[x]-1,y);
}
int qrySon(int x){
	return qry(1,id[x],id[x]+sz[x]-1)%mod;
}
void dfs1(int x,int f,int depth){
	dep[x]=depth,fa[x]=f,sz[x]=1;
	int maxson=-1e9;
	for(auto i:G[x]){
		if(i==f) continue;
		dfs1(i,x,depth+1);
		sz[x]+=sz[i];
		if(sz[i]>maxson) maxson=sz[i],son[x]=i;
	}
}
void dfs2(int x,int tp){
	id[x]=++tot,ww[tot]=w[x],top[x]=tp;
	if(!son[x]) return; dfs2(son[x],tp);
	for(auto i:G[x]){
		if(i==son[x]||i==fa[x]) continue;
		dfs2(i,i);
	}
}

signed main(){
    cin>>n>>m>>r>>mod;
    for(int i=1;i<=n;i++) cin>>w[i];
    for(int i=1,u,v;i<n;i++){
        cin>>u>>v;
        G[u].push_back(v),G[v].push_back(u);
    }
    dfs1(r,0,1),dfs2(r,r);
    bld(1,1,n);
    while(m--){
        int op,x,y,z;
        cin>>op;
        if(op==1) cin>>x>>y>>z,updRange(x,y,z);
        else if(op==2) cin>>x>>y,cout<<qryRange(x,y)%mod<<'\n';
        else if(op==3) cin>>x>>z,updSon(x,z);
        else cin>>x,cout<<qrySon(x)%mod<<'\n';
    }
    return 0;
}

P4933

定义状态 \(dp_{i,j}\) 为构成结尾为 \(h_i\),公差为 \(j\) 的等差数列的方案总数。

枚举每个电塔 \(i\),对于每个 \(j \in [1,i-1]\),结尾为 \(j\) 的等差数列接上 \(i\) 之后,公差为 \(h_i-h_j\)。于是转移为:

\[dp_{i,a_i-a_j}=\sum_{1 \le j < i} (dp_{j,a_i-a_j}+1) +1 \]

(最后在加一是因为还要算上只有 \(i\) 的区间)

实现就很简单了。时间复杂度 \(O(n^2)\)

#include<bits/stdc++.h>
using namespace std;

const int mod=998244353,p=20000;
int n,ans;
int a[1031],dp[1031][40031];

int main(){
    cin>>n;
    for(int i=1;i<=n;i++) cin>>a[i];
    for(int i=1;i<=n;i++){
        ans++;
        for(int j=1;j<i;j++){
            dp[i][a[i]-a[j]+p]+=dp[j][a[i]-a[j]+p]+1,dp[i][a[i]-a[j]+p]%=mod;
            ans+=dp[j][a[i]-a[j]+p]+1,ans%=mod;
        }
    }
    cout<<ans;
    return 0;
} 

P3038

本题几乎是树剖板子,只是点权变成了边权。

我们考虑对于每条边的边权映射到这条边的端点中较深的那个点上。

为什么不映射到较浅的边上?因为若某个节点有多个儿子,则它所有连到它儿子的边的边权都会映射到它上面,GG。

然后对于区间修改与查询,需要减去 \(\operatorname{LCA}\) 的边权,因为它映射到的边权是它的父节点连到它的边的边权,不能计入答案。

#include<bits/stdc++.h>
#define int long long
using namespace std;

int n,m,tot;
int w[400031],ww[400031];
int dep[400031],fa[400031],sz[400031],son[400031],top[400031],id[400031];
struct SegmentTree{
    int l,r,sum,add;
}t[800031];
struct EdgeInfo{
    int to,w;
};
vector<EdgeInfo> G[400031];

void bld(int p,int l,int r){
    t[p].l=l; t[p].r=r;
    if(l==r){ t[p].sum=ww[l]; return; }
    int mid=(l+r)/2;
    bld(p*2,l,mid);
    bld(p*2+1,mid+1,r);
    t[p].sum=t[p*2].sum+t[p*2+1].sum;
}
void spd(int p){
    if(t[p].add){
        t[p*2].sum+=t[p].add*(t[p*2].r-t[p*2].l+1);
        t[p*2+1].sum+=t[p].add*(t[p*2+1].r-t[p*2+1].l+1);
        t[p*2].add+=t[p].add,t[p*2+1].add+=t[p].add;
        t[p].add=0;
    }
}
void upd(int p,int l,int r,int d){
    if(l<=t[p].l&&r>=t[p].r){
        t[p].sum+=(int)d*(t[p].r-t[p].l+1);
        t[p].add+=d;
        return;
    }
    spd(p);
    int mid=(t[p].l+t[p].r)/2;
    if(l<=mid) upd(p*2,l,r,d);
    if(r>mid) upd(p*2+1,l,r,d);
    t[p].sum=t[p*2].sum+t[p*2+1].sum;
}
int qry(int p,int l,int r){
    if(l<=t[p].l&&r>=t[p].r) return t[p].sum;
    spd(p);
    int val=0;
    int mid=(t[p].l+t[p].r)/2;
    if(l<=mid) val+=qry(p*2,l,r);
    if(r>mid) val+=qry(p*2+1,l,r);
	return val;
}
void updRange(int x,int y,int z){
	while(top[x]!=top[y]){
		if(dep[top[x]]<dep[top[y]]) swap(x,y);
		upd(1,id[top[x]],id[x],z),x=fa[top[x]];
	}
	if(dep[x]>dep[y]) swap(x,y);
	upd(1,id[x],id[y],z);
    upd(1,id[x],id[x],-z);
}
int qryRange(int x,int y){
	int ans=0;
	while(top[x]!=top[y]){
		if(dep[top[x]]<dep[top[y]]) swap(x,y);
		ans+=qry(1,id[top[x]],id[x]),x=fa[top[x]];
	}
	if(dep[x]>dep[y]) swap(x,y);
	ans+=qry(1,id[x],id[y]);
    ans-=qry(1,id[x],id[x]);
	return ans;
}
void dfs1(int x,int f,int depth){
	dep[x]=depth,fa[x]=f,sz[x]=1;
	int maxson=-1e9;
	for(auto i:G[x]){
        w[i.to]=i.w;
		if(i.to==f) continue;
		dfs1(i.to,x,depth+1);
		sz[x]+=sz[i.to];
		if(sz[i.to]>maxson) maxson=sz[i.to],son[x]=i.to;
	}
}
void dfs2(int x,int tp){
	id[x]=++tot,ww[tot]=w[x],top[x]=tp;
	if(!son[x]) return; dfs2(son[x],tp);
	for(auto i:G[x]){
		if(i.to==son[x]||i.to==fa[x]) continue;
		dfs2(i.to,i.to);
	}
}

signed main(){
    cin>>n>>m;
    for(int i=1,u,v;i<n;i++){
        cin>>u>>v;
        G[u].push_back({v,0}),G[v].push_back({u,0});
    }
    dfs1(1,0,1),dfs2(1,1);
    bld(1,1,n);
    while(m--){
        char op; int x,y; cin>>op;
        if(op=='P') cin>>x>>y,updRange(x,y,1);
        else cin>>x>>y,cout<<qryRange(x,y)<<'\n';
    }
    return 0;
}