那么我们就永远不能停下脚步.|

OoXiao_QioO

园龄:2年3个月粉丝:15关注:7

平邑集训(讲课)

Day 1

CF148E

题目描述

解法

容易发现当第 i 行要取出的数字个数固定时,所能得到的最大值与其他行无关。

fi,j 表示第 i 行取 j 个数字所能得到的最大和。

容易得出:

fi,j=maxfi,j,sumi,l+sumi,kisumi,ki(jl)

那么求 f 数组的做法概括就为:枚举行(i 枚举取出的数字个数(j 枚举左边取出的数字(l)。时间复杂度 O(n3).

预处理完之后,考虑在每一组中有多少个数字,用分组背包来做。

CF95E

题目描述

解法

用并查集将每个联通快内的点数求出来,设为 xi

将题意转化为:有若干个整数(之和为 n),从中最少取出所有个数字,使得相加之和为幸运数。

将整数的数值看成体积,同时要求最少取多少个数字恰好装满体积为 x 的背包。(x 是任意一个幸运数)

用多重背包做,复杂度为 O(nmlogai)n 是物品种类,m 是枚举的背包大小。

Day 2

(补)

Day 3

(补)

Day 4

(补)

Day 5

树的直径

  • 定义:树上有边权,距离最远的两个点之间的距离(最长链)。

所以直径有两种情况:根节点到叶子节点和叶子结点到叶子结点。

求法:

  • 树形 DP

1 号节点为根,dx 表示从节点 x 出发,走向以 x 为根的子树中能够到达的最远距离。

易得 dx=maxyisonx(dyi)+dis(x,yi)

发现对于第二种情况不好求,继续设 fx 表示经过 x 节点向书中最长链的长度。

易得 fx=maxyi,yjsonx(dyi+dyj+dis(x,yi)+dis(x,yj))

其中 dis(x,yi),dis(x,yj) 就是图中的黄边。

但是复杂度会爆,因为求 f 数组的时间复杂度为 O(n2)

实际上可以在求 dy 的时候顺便求一下 fx

实现的时候可以用 ans 记录答案,省下了数组 f

点击查看代码
bool vis[200001];
int cnt,head[200001];
struct node
{
int to,nxt,w;
}e[200001];
void add(int u,int v,int w)
{
e[++cnt].to = v;
e[cnt].w = w;
e[cnt].nxt = head[u];
head[u] = cnt;
}
int ans,d[200001];
void dfs(int u)
{
vis[u] = 1;
int i;
for(i=head[u];i;i=e[i].nxt)
{
int v = e[i].to;
if(vis[v])
continue;
dfs(v);
ans = max(ans,d[u]+d[v]+e[i].w);
d[u] = max(d[u],d[v]+e[i].w);
}
return;
}
  • 贪心

也就是两遍 dfs。

1.从 1 号出发,找到距离 1 号节点最远的点 p
2.从 p 节点出发,找到距离 p 节点最远的点 q

所以从 pq 的长度就是树的直径。

贪心是要证明的,所以证明一下 p 一定是直径的一个端点。

证明:
反证法,设 p 不是直径的一个端点,而 p 是距离 1 最远的点。
1.路径有交无重合。
假设有直径 AB
因为 1P>1B,1A
所以 PB>AB
所以 AB 一定不会成为直径。
2.路径存有交有重合。
假设有直径 AB
因为 MP>AM,MB
所以 PB>AB
所以 AB 一定不会成为直径。
3.完全无交。
假设有直径 AB
所以 PM>MN+NB,不然 p 就不是距离 1 最远的点了。
PM+MN>NB
AB=AN+NB
所以 PM+MN+NA=PA>AB
所以 AB 一定不会成为直径。

注意:贪心方法不能有负边权。

点击查看代码
int cnt,head[200001];
struct node
{
int to,nxt,w;
}e[200001];
void add(int u,int v,int w)
{
e[++cnt].to = v;
e[cnt].w = w;
e[cnt].nxt = head[u];
head[u] = cnt;
}
int d[200001];
int p;
void dfs(int u,int fa)
{
int i;
for(i=head[u];i;i=e[i].nxt)
{
int v = e[i].to;
if(v==fa)
continue;
d[v] = d[u]+1;
if(d[v]>d[p])
p = v;
dfs(v,u);
}
return;
}

P3629 · 巡逻

题目描述

连接

解法

k=1 时,一定是找个最长的链,连接端点,形成环。因此直接找直径即可。答案就是 2×(n1)+1

k=2 时,第一条边就是连直径,第二条边需要分类讨论:

  • 构成一个不与第一个环相交的环

如图:

只要再贪心一次(把第一条直径上的边忽略)就可以了。

  • 构成一个与第一个环相交的环

如图:


对于这两种情况,可以一起求。具体做法:

  1. 求出直径,那么就知道了 2×(n1)+1 的值。
  2. 将直径上的边权从 1 赋值为 1,并再求出直径。

那就知道了 2×(n1)12+2,化简也就是 2×n12

代码

树网的核

原题
加强版

题目描述

给定一棵带边权无根树,在其直径上求出一段长度不超过 s 的路径 F(核),使得离路径距离最远的点到路径的距离最短(题目中的定义是偏心距)。

解法

对于本题,中心点是唯一的,对于任意一条直径而言,求出的最小偏心距都是相等的。

  • 解法一:枚举,时间复杂度 O(n3)

先求出直径的方案,枚举直径上距离不超过 s 的两个点作为核的端点,标记核上的点,从核上的每个点出发 dfs,求出核以外的每个节点到核的最短距离。

取最大值就是当前的偏心距。

不断枚举核,并求出当前路径为核的偏心距,取个最小值即可。

然后就过了原题。

  • 解法二:优化为 O(n2)

树网的核长度不超过 s,当核的一个端点确定为 p 时,另一个端点 q 应该越远越好且不超过 s

维护一个 cnt 数组即可。

  • 解法三:二分答案,优化为 O(nlogn)

剖析题目,要求最小的到核的最大距离,所以要最小化最大值,二分求解。

问题转化为,二分 mid,然后判定是否存在一个核,使得其偏心距不超过 mid

p 出发,找到一个不超过 mid 的最远点 x;再从 q 出发,找到一个不超过 mid 的最远点 y。如图:

得到一个结论: px 之间的分叉的子树,其最远点到 x 的距离不会超过到 p 的距离。而 qy 同理。

现在就剩下中间那一坨点了。

需要判断两件事:xy 距离不超过 sx,y 之间的子树上是否会超过 mid

前者显然好判断,让 x 距离 y 最近即可;后者和解法二一样。

  • 解法四:O(n)

受解法三启发,我们表示一下偏心距离。如图:

此时偏心距离为 max(dis(p,ui),dis(uj,q),x)

dui 表示从 ui 出发,不经过直径上的点能够达到最远的点的距离。显然可以预处理,时间复杂度 O(n)

若以 uiuj 为核的端点,此时偏心距为 max(disui,uj,disuj,ui,maxi<k<j(duk))

发现后半部分的 max 不能做到 O(1)

对于这个 max,可以枚举 i,找到不超过 s 的最远的 uj。发现这是个在链上的滑动窗口,直接做就行。

代码

最近公共祖先

定义太简单,不说了。

  • 向上标记法

1.让 x 一直向上走到根节点,并标记走过的点(也就是他的祖先);
2.让 y 向上走,遇到的第一个被标记过的点就是 xy 的 LCA。

所以单次找 LCA 的时间复杂度就是 O(n) 的。

  • 树上倍增法

fx,k 表示 x2k 辈祖先(k[1,log2n])。

例子:

通过手玩样例,发现 fx,k=ffx,k1,k1,而且如果祖先节点不存在,fx,k=0

具体原理也就是 \(2^k=2^{k-1]+2^{k-1}\),先走 2k1 步再走 2k1 步。

预处理 f 数组时间复杂度是 O(nlogn)

接下来就是正式求解了。

dx 表示 x 的深度,且令 dx>=dy

1.先把 x 调整到与 y 相同的深度,也就是 x 向上走 2logn,2logn1,,21,20 步。跳完后,若深度已经小于 y,选择用更小的步长走;若此时 x=y(走到了同一层),则 y 就是 LCA。
2.尝试将 x,y 同同时向上走,步长依次为 2logn,2logn1,,21,20 步。若 fx,kfy,k,则 x=fx,k,y=fy,k
3.x,y 就差一步碰面,ans=fx,0

时间复杂度 O(nlogn)

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 500000+114;
struct edge
{
int to,w,nxt;
}e[N*2];
int cnt,head[N];
int n,m,s;
void add(int u,int v)
{
++cnt;
e[cnt].to = v;
// e[cnt].w = w;
e[cnt].nxt = head[u];
head[u] = cnt;
}
int lg[N],f[N][30],d[N];
void dfs(int u,int fa)
{
f[u][0] = fa;
d[u] = d[fa]+1;
int i;
for(i=1;i<=lg[d[u]];i++)
f[u][i] = f[f[u][i-1]][i-1];
for(i=head[u];i;i=e[i].nxt)
if(e[i].to!=fa)
dfs(e[i].to,u);
return;
}
int lca(int x,int y)
{
if(d[x]<d[y])
swap(x,y);
while(d[x]>d[y])
x = f[x][lg[d[x]-d[y]]-1];
if(x==y)
return x;
int i;
for(i=lg[d[x]]-1;i>=0;i--)
if(f[x][i]!=f[y][i])
x = f[x][i],y = f[y][i];
return f[x][0];
}
int main()
{
cin>>n>>m>>s;
int i,j;
for(i=1;i<n;i++)
{
int u,v;
cin>>u>>v;
add(u,v);
add(v,u);
}
for(i=1;i<=n;i++)
lg[i] = lg[i-1]+(1<<lg[i-1]==i);
dfs(s,0);
while(m--)
{
int x,y;
cin>>x>>y;
cout<<lca(x,y)<<endl;
}
return 0;
}
  • 离线做法

dfs 一颗树的过程中,将节点分成三类:

1.已经访问过并且回溯了的点,标记为 2
2.已经访问,但没有回溯的点,标记为 1
3.尚未访问的点,不标记。

发现了一些性质:对于正在访问的点 x,从根到 x 的路径上标记都为 1,任意一个点 y,若标记为 2,则 lca(x,y) 一定是 y 向上走到根的路径中,第一个标记为 1 的点。

当一个点刚刚获得了 2 号标记,其父亲标记为 1,此时将其合并到父节点的集合。

当即将回溯到 x 时,扫描与 x 相关的所有询问(y 标记为 0),可以回答所有标记为 2yLCA(x,y)

P1967

题目描述

解法

加边方式类似最大生成树。

fx,k 表示 x2k 辈祖先,lenx,k 表示 x2k 辈祖先之间最小的边。

所以 lenx,k=min(lenx,k1,lenfx,k1,k1)

(补)

树上差分

  • 点差分

他能解决的问题:给定若干个 (x,y),表示从 x 点走到 y 点。问每个点被经过的次数?

如果在序列上,直接差分再前缀和即可。树上也是如此。

例子:

x 到根节点每个点 +1,将 y 到根节点每个点 +1,再减去 lca(x,y)falca(x,y) 到根节点的距离即可。

也就是 cnt[x]++,cnt[y]++,cnt[lca(x,y)]--,cnt[fa[lca(x,y)]]--

  • 边差分

例子:

发现边权不好做,考虑下放在点权上。

转化为:

继续考虑差分,也就是 cnt[x]++,cnt[y]++,cnt[lca(x,y)] -= 2

P3128

题目描述

解法

点差分板子。

代码

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 500000+114;
struct edge
{
int to,w,nxt;
}e[N*2];
int cntt,head[N];
int n,m,s;
void add(int u,int v)
{
++cntt;
e[cntt].to = v;
// e[cnt].w = w;
e[cntt].nxt = head[u];
head[u] = cntt;
}
int lg[N],f[N][30],d[N];
void dfs(int u,int fa)
{
f[u][0] = fa;
d[u] = d[fa]+1;
int i;
for(i=1;i<=lg[d[u]];i++)
f[u][i] = f[f[u][i-1]][i-1];
for(i=head[u];i;i=e[i].nxt)
if(e[i].to!=fa)
dfs(e[i].to,u);
return;
}
int lca(int x,int y)
{
if(d[x]<d[y])
swap(x,y);
while(d[x]>d[y])
x = f[x][lg[d[x]-d[y]]-1];
if(x==y)
return x;
int i;
for(i=lg[d[x]]-1;i>=0;i--)
if(f[x][i]!=f[y][i])
x = f[x][i],y = f[y][i];
return f[x][0];
}
int cnt[200001];
int ma = INT_MIN;
void get(int u,int fa)
{
int i;
for(i=head[u];i;i=e[i].nxt)
{
int v = e[i].to;
if(v==fa)
continue;
get(v,u);
cnt[u] += cnt[v];
}
ma = max(ma,cnt[u]);
}
int main()
{
cin>>n>>m;
s = 1;
int i;
for(i=1;i<n;i++)
{
int u,v;
cin>>u>>v;
add(u,v);
add(v,u);
}
for(i=1;i<=n;i++)
lg[i] = lg[i-1]+(1<<lg[i-1]==i);
dfs(1,0);
while(m--)
{
int x,y;
cin>>x>>y;
int l = lca(x,y);
cnt[x]++,cnt[y]++,cnt[l]--,cnt[f[l][0]]--;
}
get(1,0);
cout<<ma<<endl;
return 0;
}

P3258

AcWing 354

题目描述

解法

对于非树边 (x,y) 而言,树上肯定有一个包含 (x,y) 的环。

第一次切环上的树边,第二次一定切 (x,y)

(x,y) 所在环上的所有树边的权值都加 1

如果是树边:

  1. 边权值为 0,显然第二刀随便砍,ans+m
  2. 边权值为 1,只在一个环上,砍完以后只能砍非树边,所以 ans+1
  3. 边权值 2,则起码需要 3 刀(2 刀非树边+当前边),所以 ans 不变。

代码

Day 6

DFS 序

  • 时间戳

先了解什么时时间戳:在树的深度优先遍历中,以每个节点第一次被访问的顺序,以此给予这 n 个节点 1n 的整数标记,该标记被称为时间戳。

给个例子:

  • 树的 DFS 序

在树的深度优先遍历中,对于每个节点进入递归后以及即将回溯各记录一次该点的编号,最后产生的长度为 2n 的序列就被称为树的 DFS 序。

所以上面那棵树的 DFS 序就是

发现了一些性质:
1.每个节点在序列中恰好出现两次。
2.设 lxrx 分别表示 x 出现两次的位置,那么区间 [lx,rx] 对应的就是在树上以 x 为根的子树。

LOJ 144 · DFS 序 1

题目描述

1.树上 u 节点的权值 +x
2.询问 u 子树的权值和。

解法

代码

LOJ 145 · DFS 序 2

题目描述

1.节点 u 的子树上所有权值 +x
2.询问 u 子树的权值和。

解法

求出 DFS 序,相当于在 DFS 序上进行区间加和区间查询。

用树状数组或者带标记的线段树。

先来看看树状数组怎么区间加区间查询。在 a 数组中,[al,ar] 加上 d

b 为差分数组,所以 bl+d,br+1d

操作结束后,ax 增加的值也就是 i=1xbi

A 的前缀和 [A1,Ax] 也就是 i=1xai

A 的前缀和增加量也就是 i=1xj=1ibj

发现这个东西不好维护,先手玩一下:b1 被加了 xb2 被加了 x1b3 被加了 x2bi 被加了 xi+1

所以刚刚那个式子可以表示为 i=1x(xi+1)×bi

i=1x(xi+1)×bi

=i=1x[(x+1)×bii×bi]

=(x+1)i=1xbii=1xi×bi

发现最后要维护的就是上面那个式子的两个

开两个树状数组 T1,T2

  • 对于区间加 l,r,d

1.在 T1add(l,b)
2.在 T1add(r+1,-d)

现在知道了前半部分的 的值。

3.在 T2 中,add(l,l*d)
4.在 T2 中,add(r+1,-(r+1)*d)

现在知道了后半部分的 的值。

  • 对于区间求和 q,l,r

[sum[r]+(r+1)*ask(T1,r)-ask(T2,r)]-[sum[l-1]+l*ask(T1,l-1)-ask(T2,l-1)]

代码

LOJ 146 · DFS 序 3,树上差分 1

题目描述

1.路径权值加。
2.单点查询。
3.子树权值求和。

解法

对于操作 1 可以直接树上差分,随着操作 2 就变成了子树查询。

问题是如何解决操作 3

设差分后, v 节点的差分值是 valv。若 v 在以 u 为根的子树中,考虑 v 对于 u 这个子树权值和的增加量贡献。

得出:

valv×(depvdepu+1)=valv×depv(depu1)×valv

于是 u 的子树增加量之和就是:

(vtree(u)valv×depv)(depu1)×vtree(u)valv

代码

LOJ 147 · DFS 序 4

题目描述

1.单点加。
2.子树权值加。
3.查询路径权值和。

也就是和上一题的查询/修改反着来。

解法

我们已经知道 uv 的权值和了,所以考虑维护每个点到根的路径权值和。

对于操作 1:节点 u 增加 x,相当于以 u 为根的子树中每个点到根的路径权值和都增加了 x

对于操作 2:考虑 u 子树中的点 vv 的变化。

(depvdepu+1)×x=x×depv(depu1)×x

代码

Day 7

树上 DP

分两种情况:从根到叶子(x 是由父亲决定),从叶子到根(x 是由子树决定)。

区间 DP

步骤是:拆分区间和合并区间。

背包 DP

Day 8

(补)

Day 9

(补)

Day 10

(补)

Day 11

(补)

Day 12

(补)

Day 13

(补)

Day 14

(补)

Day 15

(补)

Day 16、17

posted @   OoXiao_QioO  阅读(13)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起