开个新坑 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)\) 的最小偏心距为
这个式子的后两项均可使用前缀和维护,重点考虑第一项如何维护。
我们有这样一个定理:
- 对于 \(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_{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\)。于是转移为:
(最后在加一是因为还要算上只有 \(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;
}