平邑集训(讲课)
Day 1
CF148E
题目描述
解法
容易发现当第 \(i\) 行要取出的数字个数固定时,所能得到的最大值与其他行无关。
设 \(f_{i,j}\) 表示第 \(i\) 行取 \(j\) 个数字所能得到的最大和。
容易得出:
\(f_{i,j} = \max{f_{i,j},sum_{i,l}+sum_{i,k_i}-sum_{i,k_i-(j-l)}}\)
那么求 \(f\) 数组的做法概括就为:枚举行(\(i\))\(\to\) 枚举取出的数字个数(\(j\))\(\to\) 枚举左边取出的数字(\(l\))。时间复杂度 \(O(n^3)\).
预处理完之后,考虑在每一组中有多少个数字,用分组背包来做。
CF95E
题目描述
解法
用并查集将每个联通快内的点数求出来,设为 \(x_i\)。
将题意转化为:有若干个整数(之和为 \(n\)),从中最少取出所有个数字,使得相加之和为幸运数。
将整数的数值看成体积,同时要求最少取多少个数字恰好装满体积为 \(x\) 的背包。(\(x\) 是任意一个幸运数)
用多重背包做,复杂度为 \(O(nm log_{a_i})\),\(n\) 是物品种类,\(m\) 是枚举的背包大小。
Day 2
(补)
Day 3
(补)
Day 4
(补)
Day 5
树的直径
- 定义:树上有边权,距离最远的两个点之间的距离(最长链)。
所以直径有两种情况:根节点到叶子节点和叶子结点到叶子结点。
求法:
- 树形 DP
设 \(1\) 号节点为根,\(d_x\) 表示从节点 \(x\) 出发,走向以 \(x\) 为根的子树中能够到达的最远距离。
易得 \(d_x=\max_{y_i \in son_x}(d_{y_i})+dis(x,y_i)\)。
发现对于第二种情况不好求,继续设 \(f_x\) 表示经过 \(x\) 节点向书中最长链的长度。
易得 \(f_x=\max_{y_i,y_j \in son_x}(d_{y_i}+d_{y_j}+dis(x,y_i)+dis(x,y_j))\)。
其中 \(dis(x,y_i),dis(x,y_j)\) 就是图中的黄边。
但是复杂度会爆,因为求 \(f\) 数组的时间复杂度为 \(O(n^2)\)。
实际上可以在求 \(d_y\) 的时候顺便求一下 \(f_x\)。
实现的时候可以用 \(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\)。
所以从 \(p\) 到 \(q\) 的长度就是树的直径。
贪心是要证明的,所以证明一下 \(p\) 一定是直径的一个端点。
证明:
反证法,设 \(p\) 不是直径的一个端点,而 \(p\) 是距离 \(1\) 最远的点。
1.路径有交无重合。
假设有直径 \(A-B\)。
因为 \(1-P>1-B,1-A\)
所以 \(P-B>A-B\)
所以 \(A-B\) 一定不会成为直径。
2.路径存有交有重合。
假设有直径 \(A-B\)。
因为 \(M-P>A-M,M-B\)
所以 \(P-B>A-B\)
所以 \(A-B\) 一定不会成为直径。
3.完全无交。
假设有直径 \(A-B\)。
所以 \(PM>MN+NB\),不然 \(p\) 就不是距离 \(1\) 最远的点了。
则 \(PM+MN>NB\)
则 \(A-B=AN+NB\)
所以 \(PM+MN+NA=PA>AB\)
所以 \(A-B\) 一定不会成为直径。
注意:贪心方法不能有负边权。
点击查看代码
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\times(n-1)-直径+1\)。
当 \(k=2\) 时,第一条边就是连直径,第二条边需要分类讨论:
- 构成一个不与第一个环相交的环
如图:
只要再贪心一次(把第一条直径上的边忽略)就可以了。
- 构成一个与第一个环相交的环
如图:
对于这两种情况,可以一起求。具体做法:
- 求出直径,那么就知道了 \(2\times(n-1)-直径+1\) 的值。
- 将直径上的边权从 \(1\) 赋值为 \(-1\),并再求出直径。
那就知道了 \(2\times(n-1)-直径1-直径2+2\),化简也就是 \(2\times n-直径1-直径2\)。
代码
树网的核
题目描述
给定一棵带边权无根树,在其直径上求出一段长度不超过 \(s\) 的路径 \(F\)(核),使得离路径距离最远的点到路径的距离最短(题目中的定义是偏心距)。
解法
对于本题,中心点是唯一的,对于任意一条直径而言,求出的最小偏心距都是相等的。
- 解法一:枚举,时间复杂度 \(O(n^3)\)。
先求出直径的方案,枚举直径上距离不超过 \(s\) 的两个点作为核的端点,标记核上的点,从核上的每个点出发 dfs,求出核以外的每个节点到核的最短距离。
取最大值就是当前的偏心距。
不断枚举核,并求出当前路径为核的偏心距,取个最小值即可。
然后就过了原题。
- 解法二:优化为 \(O(n^2)\)。
树网的核长度不超过 \(s\),当核的一个端点确定为 \(p\) 时,另一个端点 \(q\) 应该越远越好且不超过 \(s\)。
维护一个 \(cnt\) 数组即可。
- 解法三:二分答案,优化为 \(O(nlogn)\)。
剖析题目,要求最小的到核的最大距离,所以要最小化最大值,二分求解。
问题转化为,二分 \(mid\),然后判定是否存在一个核,使得其偏心距不超过 \(mid\)?
从 \(p\) 出发,找到一个不超过 \(mid\) 的最远点 \(x\);再从 \(q\) 出发,找到一个不超过 \(mid\) 的最远点 \(y\)。如图:
得到一个结论: \(p\) 到 \(x\) 之间的分叉的子树,其最远点到 \(x\) 的距离不会超过到 \(p\) 的距离。而 \(q\) 到 \(y\) 同理。
现在就剩下中间那一坨点了。
需要判断两件事:\(x\) 到 \(y\) 距离不超过 \(s\) 与 \(x,y\) 之间的子树上是否会超过 \(mid\)。
前者显然好判断,让 \(x\) 距离 \(y\) 最近即可;后者和解法二一样。
- 解法四:\(O(n)\)。
受解法三启发,我们表示一下偏心距离。如图:
此时偏心距离为 \(\max(dis(p,u_i),dis(u_j,q),x)\)
设 \(d_{u_i}\) 表示从 \(u_i\) 出发,不经过直径上的点能够达到最远的点的距离。显然可以预处理,时间复杂度 \(O(n)\)。
若以 \(u_i\) 与 \(u_j\) 为核的端点,此时偏心距为 \(\max(dis_{u_i,u_j},dis_{u_j,u_i},\max_{i<k<j}(d_{u_k}))\)。
发现后半部分的 \(\max\) 不能做到 \(O(1)\)。
对于这个 \(\max\),可以枚举 \(i\),找到不超过 \(s\) 的最远的 \(u_j\)。发现这是个在链上的滑动窗口,直接做就行。
代码
最近公共祖先
定义太简单,不说了。
- 向上标记法
1.让 \(x\) 一直向上走到根节点,并标记走过的点(也就是他的祖先);
2.让 \(y\) 向上走,遇到的第一个被标记过的点就是 \(x\) 和 \(y\) 的 LCA。
所以单次找 LCA 的时间复杂度就是 \(O(n)\) 的。
- 树上倍增法
设 \(f_{x,k}\) 表示 \(x\) 的 \(2^k\) 辈祖先(\(k\in [1,\log_2 n]\))。
例子:
通过手玩样例,发现 \(f_{x,k}=f_{f_{x,k-1},k-1}\),而且如果祖先节点不存在,\(f_{x,k}=0\)
具体原理也就是 \(2^k=2^{k-1]+2^{k-1}\),先走 \(2^{k-1}\) 步再走 \(2^{k-1}\) 步。
预处理 \(f\) 数组时间复杂度是 \(O(nlogn)\)。
接下来就是正式求解了。
设 \(d_x\) 表示 \(x\) 的深度,且令 \(d_x>=d_y\)。
1.先把 \(x\) 调整到与 \(y\) 相同的深度,也就是 \(x\) 向上走 \(2^{logn},2^{logn-1},\cdots,2^1,2^0\) 步。跳完后,若深度已经小于 \(y\),选择用更小的步长走;若此时 \(x=y\)(走到了同一层),则 \(y\) 就是 LCA。
2.尝试将 \(x,y\) 同同时向上走,步长依次为 \(2^{logn},2^{logn-1},\cdots,2^1,2^0\) 步。若 \(f_{x,k}\ne f_{y,k}\),则 \(x=f_{x,k},y=f_{y,k}\)。
3.\(x,y\) 就差一步碰面,\(ans=f_{x,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\)),可以回答所有标记为 \(2\) 的 \(y\) 的 \(LCA(x,y)\)。
P1967
题目描述
解法
加边方式类似最大生成树。
设 \(f_{x,k}\) 表示 \(x\) 的 \(2^k\) 辈祖先,\(len_{x,k}\) 表示 \(x\) 的 \(2^k\) 辈祖先之间最小的边。
所以 \(len_{x,k}=min(len_{x,k-1},len_{f_{x,k-1},k-1})\)。
(补)
树上差分
- 点差分
他能解决的问题:给定若干个 \((x,y)\),表示从 \(x\) 点走到 \(y\) 点。问每个点被经过的次数?
如果在序列上,直接差分再前缀和即可。树上也是如此。
例子:
将 \(x\) 到根节点每个点 \(+1\),将 \(y\) 到根节点每个点 \(+1\),再减去 \(lca(x,y)\) 和 \(fa_{lca(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\)。
如果是树边:
- 边权值为 \(0\),显然第二刀随便砍,\(ans+m\)。
- 边权值为 \(1\),只在一个环上,砍完以后只能砍非树边,所以 \(ans+1\)。
- 边权值 \(\ge 2\),则起码需要 \(3\) 刀(\(\ge 2\) 刀非树边+当前边),所以 \(ans\) 不变。
代码
Day 6
DFS 序
- 时间戳
先了解什么时时间戳:在树的深度优先遍历中,以每个节点第一次被访问的顺序,以此给予这 \(n\) 个节点 \(1\) 到 \(n\) 的整数标记,该标记被称为时间戳。
给个例子:
- 树的 DFS 序
在树的深度优先遍历中,对于每个节点进入递归后以及即将回溯各记录一次该点的编号,最后产生的长度为 \(2\) 到 \(n\) 的序列就被称为树的 DFS 序。
所以上面那棵树的 DFS 序就是
发现了一些性质:
1.每个节点在序列中恰好出现两次。
2.设 \(l_x\) 和 \(r_x\) 分别表示 \(x\) 出现两次的位置,那么区间 \([l_x,r_x]\) 对应的就是在树上以 \(x\) 为根的子树。
LOJ 144 · DFS 序 1
题目描述
1.树上 \(u\) 节点的权值 \(+x\)。
2.询问 \(u\) 子树的权值和。
解法
代码
LOJ 145 · DFS 序 2
题目描述
1.节点 \(u\) 的子树上所有权值 \(+x\)。
2.询问 \(u\) 子树的权值和。
解法
求出 DFS 序,相当于在 DFS 序上进行区间加和区间查询。
用树状数组或者带标记的线段树。
先来看看树状数组怎么区间加区间查询。在 \(a\) 数组中,\([a_l,a_r]\) 加上 \(d\)。
设 \(b\) 为差分数组,所以 \(b_l+d\),\(b_{r+1}-d\)。
操作结束后,\(a_x\) 增加的值也就是 \(\sum_{i=1}^x b_i\)。
\(A\) 的前缀和 \([A_1,A_x]\) 也就是 \(\sum_{i=1}^x a_i\)。
\(A\) 的前缀和增加量也就是 \(\sum_{i=1}^x \sum_{j=1}^i b_j\)。
发现这个东西不好维护,先手玩一下:\(b_1\) 被加了 \(x\),\(b_2\) 被加了 \(x-1\),\(b_3\) 被加了 \(x-2\),\(b_i\) 被加了 \(x-i+1\)。
所以刚刚那个式子可以表示为 \(\sum_{i=1}^x (x-i+1)\times b_i\)。
发现最后要维护的就是上面那个式子的两个 \(\sum\)。
开两个树状数组 \(T_1,T_2\)。
- 对于区间加 \(l,r,d\):
1.在 \(T_1\) 中 add(l,b)
。
2.在 \(T_1\) 中 add(r+1,-d)
。
现在知道了前半部分的 \(\sum\) 的值。
3.在 \(T_2\) 中,add(l,l*d)
。
4.在 \(T_2\) 中,add(r+1,-(r+1)*d)
。
现在知道了后半部分的 \(\sum\) 的值。
- 对于区间求和 \(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\) 节点的差分值是 \(val_v\)。若 \(v\) 在以 \(u\) 为根的子树中,考虑 \(v\) 对于 \(u\) 这个子树权值和的增加量贡献。
得出:
于是 \(u\) 的子树增加量之和就是:
代码
LOJ 147 · DFS 序 4
题目描述
1.单点加。
2.子树权值加。
3.查询路径权值和。
也就是和上一题的查询/修改反着来。
解法
我们已经知道 \(u\) 到 \(v\) 的权值和了,所以考虑维护每个点到根的路径权值和。
对于操作 \(1\):节点 \(u\) 增加 \(x\),相当于以 \(u\) 为根的子树中每个点到根的路径权值和都增加了 \(x\)。
对于操作 \(2\):考虑 \(u\) 子树中的点 \(v\),\(v\) 的变化。
代码
Day 7
树上 DP
分两种情况:从根到叶子(\(x\) 是由父亲决定),从叶子到根(\(x\) 是由子树决定)。
区间 DP
步骤是:拆分区间和合并区间。
背包 DP
Day 8
(补)
Day 9
(补)
Day 10
(补)
Day 11
(补)
Day 12
(补)
Day 13
(补)
Day 14
(补)
Day 15
(补)