为了表明我还活着
SSSP(Single Sourse Shortest Path) 问题
dijkstra
void dijkstra() {
memset(dis, 0x3f, sizeof dis);
memset(vis, 0, sizeof vis);
dis[1] = 0;
queue.push(make_pair(0,1));
while(queue.size()) {
int x=q.top().second(); q.pop();
if(vis[x]) continue;
vis[x] = true;
for(int i=head[x]; i; i=next[i]) {
int y=ver[i], z=weight[i];
if(dis[y] > dis[x]+z) {
dis[y] = dis[x]+z;
queue.push( make_pair(-dis[y],y) );
}
}
}
}
队列优化 Bellman-Ford
void spfa() {
memset(dis, 0x3f, sizeof dis);
memset(inqueue, 0, sizeof inqueue);
dis[1]=0, inqueue[1]=true;
queue.push(1);
while(queue.size()) {
int x=queue.front(); queue.pop();
inqueue[x] = false;
for(int i=head[x]; i; i=next[i]) {
int y=ver[i], z=weight[i];
if(dis[y] > dis[x]+z) {
dis[y] = dis[x]+z;
if(!inqueue[y]) queue.push(y), inqueue[y]=true;
}
}
}
}
解法1 :图论建模+最短路
// 过于简单, 不写。
解法2:二分答案+双端队列 BFS
// 重点是双端队列 BFS 解边权只有 0/1 的最短路, 理解这个算法需要对 BFS 时队列里节点之间的数据特点有适当的了解, 在此不叙。
本题的重点是在对题目的分析上, 代码编写上对于初学者的难度主要集中在各种重要的细节上, 是很好的入门题。
本题的分析比较简单, 重点是在代码上, 即使是对于水平较差的提高选手也值得一写。
APSP(All-pairs shortest path) 问题
Floyd
其中,dis[k,i,j] 表示从 i 到 j, 路径上除了两端点编号不超过 k, 得到的最短路的长度。
void Floyd() {
for(int k=1; k<=n; ++k)
for(int i=1; i<=n; ++i)
for(int j=1; j<=n; ++j)
dis[i][j] = min(dis[i][j], dis[i][k]+dis[k][j]);
}
本体的重点是了解各种常见关系(如偏序关系)的特征。
本题对于真正的“思考”上的初学者, 是一道不错的练习题, 同时也助于初识 Floyd 算法的人加深理解。
本题也有助于加深对 Floyd 一些特性的记忆。
简单说一下:对于 dis(i,j), 用不同的 k 来更新, 所涉及的路径彼此没有交集。
生成树相关
定理
1.任意一颗最小生成树一定包含无向图中最小的边
推论:切割性质
将原图的一个生成森林加边变成生成树, 要求边权和尽量小, 则这颗生成树一定包含连接两个不联通的子图的边中的最小的边
Kruskal 基于上述推论, 维护图的最小生成森林, 时间复杂度为 O(m log m) 。
Prim 算法也基于上述推论, 但它的思路是维护图的最小生成树的一部分, 时间复杂度为 O(m log n), 显然, Prim 在稠密图上效果优于 Kruskal 。
// 未加堆优化, 复杂度 O(n^2)
void prim() {
memset(dis, 0x3f, sizeof dis);
memset(vis, 0, sizeof vis);
dis[1] = 0;
for(int i=1; i<n; ++i) {
int x = 0;
for(int j=1; j<=n; ++j)
if(!vis[j] && (x==0||d[j]<d[x]) ) x=j;
vis[x] = 1;
for(int y=1; y<=n; ++y)
if(!vis[y]) d[y] = min(d[y], a[x][y]);
}
}
最小瓶颈生成树
一棵生成树, 最大边权尽量小。
最小生成树就是一个最小瓶颈生成树; 不是所有的最小瓶颈生成树都是最小生成树。
最小瓶颈路
求一条 u->v 的路径,最大边尽量小。
答案就是在最小瓶颈生成树上的唯一路径。
单独考虑每一条新加进去的边, 都与原树构成一个环, 为使总边权最小且唯一最小生成树不变, edge(x,y).weight 应是原树中 road(x,y).maxweight+1。
关于如何统计所有边权和, 有一个很具有启发意义的算法:
将原树的边按照边权从小到大的顺序依次加到图中, 每次加边都会联通两个联通块, 除了这条边以外, 所有连接这两个联通块的边都与树构成一个环, 环上的最大路径是本次加的边(注意这时边已经加上了), 统计即可。
求图的最小生成树, 满足 1 号节点的度数不超过 S。
考虑最后加与 1 号节点相关的边:首先去掉 1 号节点, 对剩下的联通块分别求出最小生成树, 并试图用不超过 S 条边将图联通, 据定理, 这几条边应尽量短。最后尝试调整。
算法的正确性:最初看到这个算法我也是懵逼的, 因为原文的叙述方法是有问题的, 应该先讲将图联通, 这样就好理解了。
0/1 分数规划问题, 待补。
最短路+计数
最终答案一定是加了几条边, 构成几个联通块, 几个联通块内部点权和都是 0。
显然如果固定几个点要形成联通块, 最小代价一定是这些点生成子图的最小生成树的边权之和。
预处理出所有联通块, 做一个类似背包的算法吗可以保证最优解一定会这样的算法覆盖到。
代码暂时不写了。
树的重心
定义 相比于删去其他节点,删去重心后的剩下的最大子树的值最小。
性质
0.删去重心后的所有子树节点数都不超过原树的 \(\dfrac{n}2\)
1.一棵树最多有两个重心, 且相邻。
2.所有节点到重心的距离和最小, 对于两个重心, 相等。
3.两个树用一条边连起来, 新的重心在连接原两棵树的重心的路径上。
4.树添加或删除一个叶子节点, 重心最多只会移动到相邻的节点。
找到一篇观感不错的博文, 是对树的重心的一部分性质的证明, 点 这里 可以看到。
这里我也简单证明一下:(这里除法都是整除)
由性质 0 推演出性质 1~4, 这个性质太强啦!
0.朝子树节点数超过 \(\dfrac{n}2\) 的子树走一步, 明显更可能成为重心。
1.如果树上存在两个不相邻的重心 x,y, 它们往对方的方向各走一步, 成为 x' 和 y' , 由树的重心的性质 0 可知, \(siz[x^{'}] + siz[y^{'}] \le n\), 然而此时(x,y不相邻)这两个东西加起来明显是 n+一个非零的数值, 所以矛盾了, 树上不存在两个不相邻的重心, 即所有重心要么相等要么互相相邻, 故不存在两个以上的重心, 然而确实有树有两个重心, 命题自然得证。
2.由性质 0 ,将两个重心之间的边断开, 所得两个子树节点数相等, 由此只需证明所有节点到重心的距离和最小。这个也挺显然的, 因为非重心必有一个子树大小超过 \(\dfrac{n}2\) , 所以朝这个超过的子树走, 距离和一定会变小, 也就是说非重心的距离和一定不是最小的。
话说考虑接下来两条的时候我把 n 当成常数错了一遍。
3.其实挺显然的, 具体设计个量算算, 把不在路径上的点排除就行了。
4.原先的 \(\dfrac{n}2\) 可能等于 \(\dfrac{n-1}2\) , 这样加入节点后就变成 \(\dfrac{n+1}2\) , 这两者之间相差 1, 重心不移动。
如果 \(\dfrac{n}2\) 就是 \(\dfrac{n}2\) , 大于 \(\dfrac{n-1}2\) , 那么加入节点后还是 \(\dfrac{n}2\) , 重心可能移动, 但显而易见最多移动一次。
基于这样一个事实:
对于固定的 x,y, min{d(v,x), d(v,y)} 取到 d(v,x) 还是 d(x,y) 是以树中的一条边为界的。
得出一个乱搞做法:枚举为界的边断开, 每枚举一条边就在剩下的两颗树里分别算最小值然后加起来, 最后取最小值。
显然题目要求的答案是 \(\ge\) 这样求出来的答案的, 然而答案是否是这个呢?即, 这样求出来的最优的 x,y,分界边还是那个断边吗?
我目前没有能力证明这个, 就只好写了, 网上题解中也是这个做法, 也没有证明, 草。
这道题的代码部分还是挺值得一写的。 对于初学者来说
ssssss Sss
首先直接做很难求, 考虑 \(\sum_{a \in S} a = \sum_{a} a*a在S中的出现次数\)这个经典式子, 统计每个点 x 会成为多少个重心。
由于 x 和另一个子树的连线上是原树的重心, 所以把原树重心作为根, 统计有多少边,在删掉它们后(这条边靠近根的那个点与 x 之间的路径上要有根节点), x 会是重心。
然后就很好统计了(吧
树的直径
性质
0.到一个点距离最长的点是直径端点之一
1.将两棵树用一条边连起来, 新的直径的端点一定是这两棵树原来的直径的端点。
2.给一棵树接上一个叶子节点, 直径最多只会改变一个端点
3.若一棵树存在多个直径, 则这些直径交于一点且这个点是这些直径的中点。
树形 DP 求直径
void dp(int x) {
vis[x] = 1;
for(int i=head[x]; i; i=next[i]) {
int y=ver[i], z=weight[i];
if(vis[y]) continue;
dp(y);
ans = max(ans,d[y]+z+d[x]);
d[x] = max(d[x],d[y]+z);
}
}
BFS 求直径
// 没法抄, 不写了。
同时,为了不浪费资金,每天巡警车必须经过新建的道路正好一次。
读到这里感觉有点生草。
这道题没什么可说的, 唯一的小坑就是 “图不是个二维结构”。