图论是一个综合性很强,实用性和研究性兼具的板块,是算法竞赛常考的东东
图论算法本身不难,难在对于问题的转化与建模,譬如如何想到这个问题是考察的图论知识,该如何建立一张图来解决问题,本文会在介绍知识点的同时,介绍经典的套路以及思维模式。
基本概念:由于无向边可以看作两条相反的有向边,于是我们默然按照有向边的形式讨论
存图方式:
scanf("%d%d%d",&u,&v,&w);
a[u][v]=w;//邻接矩阵
void add(int u,int v,int w){//u->v的边权为w的边
nxt[++tot]=head[u],ver[tot]=v,cost[tot]=w,head[u]=tot;
}
//遍历时:
for(int i=head[u];i;i=nxt[i])……
为了方便叙述,下面没特殊说明默认 d i s t dist dist数组为存储距离的数组, c o s t u , v cost_{u,v} costu,v表示边 u − > v u->v u−>v的长度,默认使用邻接表存图,默认 u u u为父节点, v v v为子节点
应用于非负权图上,基于贪心思想
它的过程是这样的
3.直到堆为空时,算法结束
实现细节
1.如果懒得重载运算符,建议把 d i s t dist dist值跟随节点入队时取反,拿出来时再取反,这就可以吧默认大根堆改为小根堆
2.时刻记得memset(dist,0x3f,sizeof dist);
代码如下:
// 堆优化Dijkstra算法,O(mlogn)
const int N = 100010, M = 1000010;
int head[N], ver[M], edge[M], Next[M], d[N];
bool v[N];
int n, m, tot;
// 大根堆(优先队列),pair的第二维为节点编号
// pair的第一维为dist的相反数(利用相反数变成小根堆,参见0x71节)
priority_queue< pair > q;
void add(int x, int y, int z) {
ver[++tot] = y, edge[tot] = z, Next[tot] = head[x], head[x] = tot;
}
void dijkstra() {
memset(d, 0x3f, sizeof(d)); // dist数组
memset(v, 0, sizeof(v)); // 节点标记
d[1] = 0;
q.push(make_pair(0, 1));
while (q.size()) {
// 取出堆顶
int x = q.top().second; q.pop();
if (v[x]) continue;
v[x] = 1;
// 扫描所有出边
for (int i = head[x]; i; i = Next[i]) {
int y = ver[i], z = edge[i];
if (d[y] > d[x] + z) {
// 更新,把新的二元组插入堆
d[y] = d[x] + z;
q.push(make_pair(-d[y], y));
}
}
}
}
int main() {
cin >> n >> m;
// 构建邻接表
for (int i = 1; i <= m; i++) {
int x, y, z;
scanf("%d%d%d", &x, &y, &z);
add(x, y, z);
}
// 求单源最短路径
dijkstra();
for (int i = 1; i <= n; i++)
printf("%d\n", d[i]);
}
它的复杂度是 O ( ( m + n ) log n ) O((m+n)\log n) O((m+n)logn)
在国际上, S P F A SPFA SPFA其实被称为队列优化的 B e l l m a n − f o r d Bellman-ford Bellman−ford算法,这两种算法也是求解单源最短路径问题的算法,与 d i j k s t r a dijkstra dijkstra算法不同的是,这两种算法在负权图上一样可以正常工作,由于 S P F A SPFA SPFA是 B e l l m a n − f o r d Bellman-ford Bellman−ford算法的优化形式,下面就只讲述 S P F A SPFA SPFA算法
首先,在一张有向图上,若是对于任意边 ( u , v , w ) (u,v,w) (u,v,w)都有: d i s t [ u ] + w ≥ d i s t [ v ] dist[u]+w\ge dist[v] dist[u]+w≥dist[v],(此不等式又叫三角形不等式),那么我们所求的 d i s t dist dist数组就是最短路径,而 S P F A SPFA SPFA算法便是通过若干次更新迭代,使所有边都满足三角形不等式,下面我们介绍它的详细流程
// SPFA算法
const int N = 100010, M = 1000010;
int head[N], ver[M], edge[M], Next[M], d[N];
int n, m, tot;
queue q;
bool v[N];
void add(int x, int y, int z) {
ver[++tot] = y, edge[tot] = z, Next[tot] = head[x], head[x] = tot;
}
void spfa() {
memset(d, 0x3f, sizeof(d)); // dist数组
memset(v, 0, sizeof(v)); // 是否在队列中
d[1] = 0; v[1] = 1;
q.push(1);
while (q.size()) {
// 取出队头
int x = q.front(); q.pop();
v[x] = 0;
// 扫描所有出边
for (int i = head[x]; i; i = Next[i]) {
int y = ver[i], z = edge[i];
if (d[y] > d[x] + z) {
// 更新,把新的二元组插入堆
d[y] = d[x] + z;
if (!v[y]) q.push(y), v[y] = 1;
}
}
}
}
int main() {
cin >> n >> m;
// 构建邻接表
for (int i = 1; i <= m; i++) {
int x, y, z;
scanf("%d%d%d", &x, &y, &z);
add(x, y, z);
}
// 求单源最短路径
spfa();
for (int i = 1; i <= n; i++)
printf("%d\n", d[i]);
}
在随机图的情况下,SPFA算法的时间复杂度是 O ( k n ) O(kn) O(kn),其中k是一个较小的常数,但可以构造数据卡,使其退化到 O ( n m ) O(nm) O(nm)级别,并且之前我在知乎上看到一篇文章,几乎所有的SPFA算法优化都在上面被卡了
同样,正是因为SPFA使得三角形不等式绝对收敛,以至于其可以在负权图上工作,当位于正权图的时候,一样可以采取堆优化,最后得到的算法与dijkstra算法一模一样,一个是基于贪心的非负权算法,一个是基于三角形不等式收敛的通用算法,在非负权图上却是殊途同归
简单来说就是在一张图上要求出任意两点间的最短路径,我们使用Floyd算法,时间复杂度为 O ( n 3 ) O(n^3) O(n3),一般使用邻接矩阵存图
使用动态规划的思想,设 D [ k , i , j ] D[k,i,j] D[k,i,j]表示i到j的路径只经过节点编号小于k的路径的最短长度,则有状态转移方程
D [ k , i , j ] = min { D [ k − 1 , i , j ] , D [ k − 1 , i , k ] + D [ k − 1 , k , j ] } D[k,i,j]=\min \lbrace D[k-1,i,j],D[k-1,i,k]+D[k-1,k,j]\rbrace D[k,i,j]=min{D[k−1,i,j],D[k−1,i,k]+D[k−1,k,j]},类似于01背包那样,k这一维度可以省略
值得一提的是,在动态规划算法中,对k的枚举才是阶段,i,j只是附加状态而已。于是循环顺序应该是 k − > i − > j k->i->j k−>i−>j
int d[310][310];
int n, m;
int main() {
cin >> n >> m;
// 把d数组初始化为邻接矩阵
memset(d, 0x3f, sizeof(d));
for (int i = 1; i <= n; i++) d[i][i] = 0;
for (int i = 1; i <= m; i++) {
int x, y, z;
scanf("%d%d%d", &x, &y, &z);
d[x][y] = min(d[x][y], z);
}
// floyd求任意两点间最短路径
for (int k = 1; k <= n; k++)
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++)
d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
// 输出
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) printf("%d ", d[i][j]);
puts("");
}
}
传递闭包问题是这样的:给定 n n n个元素和 m m m个二元关系,关系具有传递性,求推出最多的关系。
建立邻接矩阵 d d d,如果 i , j 有关系,那么令 d [ i , j ] = 1 i,j\text{有关系,那么令}d[i,j]=1 i,j有关系,那么令d[i,j]=1,特别的,令 d [ i , i ] = 1 d[i,i]=1 d[i,i]=1,其余均为0,对d执行 F l o y d Floyd Floyd即可求出传递闭包关系,它的原理是Floyd本身的递推式就是在做关系的传递(仅属个人见解)
// Floyd传递闭包
#include
#include
#include
#include
using namespace std;
bool d[310][310];
int n, m;
int main() {
cin >> n >> m;
for (int i = 1; i <= n; i++) d[i][i] = 1;
for (int i = 1; i <= m; i++) {
int x, y;
scanf("%d%d", &x, &y);
d[x][y] = d[y][x] = 1;
}
for (int k = 1; k <= n; k++)
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++)
d[i][j] |= d[i][k] & d[k][j];
}
题意:在无向图上求出一条1到n的路径,使得第K+1大的边权尽量小,共有N个点P条边
介绍两种解法,两种思想
1.分层图最短路
具体的,我们利用动态规划的思想,设 d [ i , j ] d[i,j] d[i,j]表示1到i的路径中第j+1大的边权的最小值,于是对于一条边 ( u , v , w ) (u,v,w) (u,v,w)我们可以用 max ( w , d [ u , k ] ) \max(w,d[u,k]) max(w,d[u,k])去更新 d [ v , k ] d[v,k] d[v,k],用 d [ u , k ] d[u,k] d[u,k]去更新 d [ v , k + 1 ] d[v,k+1] d[v,k+1]。在我的另一篇文章中曾经提到过DP实际上也是对状态空间进行的一张有向无环图的遍历,显然这个方程有后效性,但是我们可以使用SPFA去消去后效性,因为三角形不等式会令其不断收敛,但又因为全是正权图的原因就可以使用堆优化。
如果从图论的角度来看,无疑我们可以把点的状态抽象为二维的,用二元组 ( u , k ) (u,k) (u,k)代表一个节点,那么就等同于节点 ( u , k ) (u,k) (u,k)与节点 ( v , k ) (v,k) (v,k)有着一条边权为 w w w的边,与节点 ( v , k + 1 ) (v,k+1) (v,k+1)有着一条边权为0的边,这是一个具有N * K个点,P * K条边的广义最短路问题,又被称为分层图最短路
#define x second.first
#define y second.second
#define mp make_pair
int head[1050],ver[105000],cost[105000],nxt[105000],tot,n,m,k,dist[1050][1050],vis[1050][1050];
priority_queue > >q;
void add(int u,int v,int w) {
ver[++tot]=v,cost[tot]=w,nxt[tot]=head[u],head[u]=tot;
}
void dijkstra(){
memset(dist,0x3f,sizeof(dist));
dist[1][0]=0;
q.push(mp(0,mp(1,0)));
while (!q.empty()) {
int u=q.top().x,dep=q.top().y;
q.pop();
if (vis[u][dep])continue;
vis[u][dep]=1;
for (int i=head[u];i;i=nxt[i]) {
int v=ver[i],w=cost[i];
if (dist[v][dep]>max(dist[u][dep],w)) {
dist[v][dep]=max(dist[u][dep],w);
q.push(mp(-dist[v][dep],mp(v,dep)));
}
if (dep < k && dist[v][dep+1]>dist[u][dep]) {
dist[v][dep+1]=dist[u][dep];
q.push(mp(-dist[v][dep+1],mp(v,dep+1)));
}
}
}
}
int main() {
scanf("%d%d%d",&n,&m,&k);
for (int i=1;i<=m;i++) {
int u,v,w;
scanf("%d%d%d",&u,&v,&w);
add(u,v,w);
add(v,u,w);
}
dijkstra();
printf("%d\n",dist[n][k]==0x3f3f3f3f?-1:(k!=n?dist[n][k]:0));//最后一组数据:N=2K=2????不是K
2.二分转化判定
很明显的单调性,我们可以二分第K+1大的边权的最小值,设为 m i d mid mid
然后我们把所有边权大于 m i d mid mid的边权看作1,小于等于的看作0,使用双端队列bfs在 O ( n ) O(n) O(n)时间内求出最短路,当最短路超过k的时候上界收敛,否则下界收敛
// 解法二:二分答案+双端队列BFS
const int MAX_N = 1005, MAX_M = 20005;
int head[MAX_N], ver[MAX_M], edge[MAX_M], nxt[MAX_M], tot;
int n, m, k, d[MAX_N];
deque q;
// 插入一条从x到y长度z的有向边
void add(int x, int y, int z) {
tot++;ver[tot] = y; edge[tot] = z;nxt[tot] = head[x],head[x]=tot; // 在head[x]这条链的开头插入新点
}
bool solve(int t) {
memset(d, 0x7f, sizeof(d));
d[1] = 0;
q.push_back(1);
while (!q.empty()) {
int x = q.front();
q.pop_front();
for (int i = head[x]; i; i = nxt[i]) {
int y = ver[i], z = edge[i] > t ? 1 : 0;
if (d[y] > d[x] + z) {
d[y] = d[x] + z;
if (z == 0) q.push_front(y); else q.push_back(y);
}
}
}
return d[n] <= k;
}
int main() {
cin >> n >> m >> k;
for (int i = 1; i <= m; i++) {
int x, y, z;
scanf("%d%d%d", &x, &y, &z);
add(x, y, z);
add(y, x, z);
}
int L = 0, R = 1000001;
while (L < R) {
int mid = (L + R) >> 1;
if (solve(mid)) R = mid; else L = mid + 1;
}
if (L == 1000001) puts("-1"); else cout << L << endl;
}
这道题告诉我们的是分层图的最短路,借用动态规划思想解决最短路径问题,还有第x大的最小值是二分,可以抽象为二维节点,也可以把每一个节点的边都建立出来
题目描述:
分析:本题是一个带有负权的单源最短路径问题,但是SPFA被卡了,于是我们需要运用图上的性质:道路都是双向的,航线都是单向的,且如果一条航线从A->B,那么不可能存在一条道路使得B->A,这句话的意思就是说任何一条航线都是这张图的一个割边,并且只有这个割边的边权为负数,对于这样的图,我们可以拆图,将这张图拆为一个个连通块和割边,详细的说,我们最开始无视所有的航线,只进行对道路的计算,无疑,这些道路全是正权,我们可以使用dijkstra算法求出一个个连通块内的最短路,又由于割边的存在,我们把每一个连通块看成一个点,那么这张图就成了一个 D A G DAG DAG,我们就可以使用线性的动态规划求出答案,这个顺序需要我们预处理出拓扑序
流程如下:
#include
#include
#include
#include
#include
#define int long long
using namespace std;
queueq;
priority_queue >p;
int t,r,n,m,P,s,cnt,head[300005],nxt[300005],ver[300005],cost[300005],tot,vis[300005],c[300005],deg[300005],kind[300005],dist[300005];
vectora[300005];
void add(int u,int v,int w,int k){
nxt[++tot]=head[u],ver[tot]=v,cost[tot]=w,head[u]=tot,kind[tot]=k;//1是单向边,0是有向边
}
void dfs(int u,int id){
c[u]=id;
a[id].push_back(u);
for(int i=head[u];i;i=nxt[i]){
if(kind[i])continue;
if(!c[ver[i]])dfs(ver[i],id);
}
}
void dijkstra(int id){//求编号为id的联通块内最短路
int len=a[id].size();
for(int i=0;iw+z){
dist[v]=w+z;
p.push(make_pair(-dist[v],v));
}
if(c[v]!=c[u]){
dist[v]=min(w+z,dist[v]);
deg[c[v]]--;
if(!deg[c[v]])q.push(c[v]);
}
}
}
}
signed main(){
int maxx=0;
scanf("%lld%lld%lld%lld",&n,&m,&P,&s);
for(int i=1;i<=m;i++){
int u,v,w;
scanf("%lld%lld%lld",&u,&v,&w);
add(u,v,w,0);
add(v,u,w,0);
}
for(int i=1;i<=P;i++){
int u,v,w;
scanf("%lld%lld%lld",&u,&v,&w);
add(u,v,w,1);
}
for(int i=1;i<=n;i++){
if(!c[i])dfs(i,++cnt);
}
for(int u=1;u<=n;u++){
for(int i=head[u];i;i=nxt[i]){
if(kind[i]){
deg[c[ver[i]]]++;
}
}
}
for(int i=1;i<=cnt;i++){
if(!deg[i])q.push(i);
}
for(int i=1;i<=n;i++)dist[i]=0x3f3f3f3f3f3f3f3f;
dist[s]=0;
while(q.size()){
int k=q.front();
q.pop();
dijkstra(k);
}
for(int i=1;i<=n;i++){
if(dist[i]<=n*10000)printf("%lld\n",dist[i]);
else puts("NO PATH");
}
return 0;
}
这道题告诉我们,合理运用题目性质,对于图的明显性质,我们可以将一张图分为几个块,使用不同的算法进行处理,本题就是将连通块内使用dijkstra,然后缩点成DAG使用线性递推直接求解
题意简述:给定一张有向图,求起点S到终点F的最短路径条数和次短路径条数
对于这个问题的维护,我们只需要把dijkstra算法稍稍改造一下就可以了
我们开一个数组 c n t cnt cnt,表示路径条数,同时 d i s t , c n t dist,cnt dist,cnt都是二维,第二维只能是0/1表示最短路/次短路
下面我们简单谈谈它的维护
首先在优先队列里我们记录当前状态是由次短路还是最短路更新,记为k,那么在我们执行正常dijkstra算法的时候就会出现四种情况
1. d i s t [ v ] [ 0 ] > d i s t [ u ] [ k ] + w dist[v][0]>dist[u][k]+w dist[v][0]>dist[u][k]+w,此时令 d i s t [ v ] [ 1 ] = d i s t [ v ] [ 0 ] , d i s t [ v ] [ 0 ] = d i s t [ u ] [ k ] , c n t [ v ] [ 1 ] = c n t [ v ] [ 0 ] , c n t [ v ] [ 0 ] = c n t [ u ] [ k ] dist[v][1]=dist[v][0],dist[v][0]=dist[u][k],cnt[v][1]=cnt[v][0],cnt[v][0]=cnt[u][k] dist[v][1]=dist[v][0],dist[v][0]=dist[u][k],cnt[v][1]=cnt[v][0],cnt[v][0]=cnt[u][k],并将两个都插入队列
2. d i s t [ v ] [ 1 ] > d i s t [ u ] [ k ] + w > d i s t [ v ] [ 0 ] dist[v][1]>dist[u][k]+w>dist[v][0] dist[v][1]>dist[u][k]+w>dist[v][0],此时令 d i s t [ v ] [ 1 ] = d i s t [ u ] [ k ] + w dist[v][1]=dist[u][k]+w dist[v][1]=dist[u][k]+w,
3. d i s t [ v ] [ 0 ] = d i s t [ u ] [ k ] + w dist[v][0]=dist[u][k]+w dist[v][0]=dist[u][k]+w
4. d i s t [ v ] [ 1 ] = d i s t [ u ] [ k ] + w dist[v][1]=dist[u][k]+w dist[v][1]=dist[u][k]+w
只有在这四种情况下才会更新
const int N=1010,M=100010;
int T,t,n,m,s,head[N],ver[M],cost[M],nxt[M],tot,d[N][2],cnt[N][2],vis[N][2];
struct node{
int id,k,d;
bool operator<(const node& node)const {
return d>node.d;
}
};
priority_queueq;
void add(int u,int v,int w){
ver[++tot]=v,cost[tot]=w,nxt[tot]=head[u],head[u]=tot;
}
int dijkstra(){
memset(vis,0,sizeof vis);
memset(d,0x3f,sizeof d);
d[s][0]=0,cnt[s][0]=1;
q.push({s,0,0});
while(q.size()){
int u=q.top().id,k=q.top().k,z=d[u][k];q.pop();
if(vis[u][k])continue;
vis[u][k]=true;
for(int i=head[u];i;i=nxt[i]){
int v=ver[i],w=cost[i];
if(d[v][0]>z+w){
d[v][1]=d[v][0],cnt[v][1]=cnt[v][0];
q.push({v,1,d[v][1]});
d[v][0]=z+w,cnt[v][0]=cnt[u][k];
q.push({v,0,d[v][0]});
}
else if(d[v][0]==z+w)cnt[v][0]+=cnt[u][k];
else if(d[v][1]>z+w){
d[v][1]=z+w,cnt[v][1]=cnt[u][k];
q.push({v,1,d[v][1]});
}
else if(d[v][1]==z+w)cnt[v][1]+=cnt[u][k];
}
}
//printf("%d %d %d %d\n",d[t][0],cnt[t][0],d[t][1],cnt[t][1]);
return cnt[t][0]+(d[t][1]==d[t][0]+1)*cnt[t][1];
}
int main(){
scanf("%d",&T);
while(T--){
tot=0;
memset(head,0,sizeof head);
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++){
int u,v,w;
scanf("%d%d%d",&u,&v,&w);
add(u,v,w);
}
scanf("%d%d",&s,&t);
printf("%d\n",dijkstra());
}
return 0;
}
一个不同的值的升序排序数列指的是一个从左到右元素依次增大的序列,例如,一个有序的数列 A , B , C , D A,B,C,D A,B,C,D 表示 A < B , B < C , C < D AA<B,B<C,C<D。在这道题中,我们将给你一系列形如 A < B AA<B 的关系,并要求你判断是否能够根据这些关系确定这个数列的顺序。
第一行有两个正整数 n , m n,m n,m, n n n 表示需要排序的元素数量, 2 ≤ n ≤ 26 2\leq n\leq 26 2≤n≤26,第 1 1 1 到 n n n 个元素将用大写的 A , B , C , D … A,B,C,D\dots A,B,C,D… 表示。 m m m 表示将给出的形如 A < B AA<B 的关系的数量。
接下来有 m m m 行,每行有 3 3 3 个字符,分别为一个大写字母,一个 <
符号,一个大写字母,表示两个元素之间的关系。
若根据前 x x x 个关系即可确定这 n n n 个元素的顺序 yyy..y
(如 ABC
),输出
Sorted sequence determined after xxx relations: yyy...y
.
若根据前 x x x 个关系即发现存在矛盾(如 A < B , B < C , C < A AA<B,B<C,C<A),输出
Inconsistency found after x relations.
若根据这 m m m 个关系无法确定这 n n n 个元素的顺序,输出
Sorted sequence cannot be determined.
(提示:确定 n n n 个元素的顺序后即可结束程序,可以不用考虑确定顺序之后出现矛盾的情况)
2 ≤ n ≤ 26 , 1 ≤ m ≤ 600 2 \leq n \leq 26,1 \leq m \leq 600 2≤n≤26,1≤m≤600。
很明显的,这是一道传递闭包问题,对于 i < j i
套传递闭包即可。至于确定顺序之后输出,我们只需要将有多少个数小于当前数统计出来,这就是实质上的排名
注意这里不可以使用二分法,是因为二分很可能出现前面可以确定顺序后面发生矛盾却是二分到了矛盾的地方
int e[55][55],d[55][55],n,m;
pairans[105];
int floyd(){
memcpy(e, d, sizeof(e));
for(int k=1;k<=n;k++)
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++){
e[i][j]|=e[i][k]&e[k][j];
if(e[i][j]&&e[j][i])return -1;
}
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++){
if(e[i][j]==e[j][i]&&!e[i][j]&&i!=j)return 0;
}
return 1;
}
int main(){
while(~scanf("%d%d",&n,&m)&&n&&m){
int op=1;
memset(d,0,sizeof d);
for(int i=1;i<=m;i++){
char a[3];
scanf("%s",a);
d[(int)a[0]-64][(int)a[2]-64]=1;
if(op==1){
int x=floyd();
if(x==1){
printf("Sorted sequence determined after %d relations: ",i);
for(int i=1;i<=n;i++){
ans[i].first=0;
for(int j=1;j<=n;j++){
if(!e[i][j])ans[i].first++;
}
ans[i].second=(char)(i+64);
}
sort(ans+1,ans+n+1);
for(int i=1;i<=n;i++){
printf("%c",ans[i].second);
}
puts(".");
op=0;
}
else if(x==-1){
printf("Inconsistency found after %d relations.\n",i);
op=0;
}
}
}
if(op){
puts("Sorted sequence cannot be determined.");
}
}
}
这是经典的最小环问题
给定一张无向图,求图中一个至少包含 3 个点的环,环上的节点不重复,并且环上的边的长度之和最小。
该问题称为无向图的最小环问题。
你需要输出最小环的方案,若最小环不唯一,输出任意一个均可。
这里我们使用Floyd算法,因为Floyd算法外层循环的k代表节点编号不超过k,于是一个环可以表示为: f [ i ] [ j ] + a [ i ] [ k ] + a [ k ] [ j ] f[i][j]+a[i][k]+a[k][j] f[i][j]+a[i][k]+a[k][j],当然,这是还没有进行第k层递推的时候,我们就相当于是枚举了k在环上的左右边,当然不相连的时候就是无穷大。至于方案递归输出就可
这样枚举完取最小值得到的环是满足以下条件的最小环
1.经过k
2.由编号不超过k的节点构成
由对称性可知,这样做不会影响结果
int ans[10005],num,cnt,d[105][105],a[105][105],h[105][105],m,n;
long long sum=0x3f3f3f3f;
void get_ans(int i,int j){
if(h[i][j]==0)return ;
get_ans(i,h[i][j]);
ans[++num]=h[i][j];
get_ans(h[i][j],j);
}//求方案
void floyd(){
for(int k=1;k<=n;k++){
for(int i=1;id[i][k]+d[k][j])h[i][j]=k,d[i][j]=d[i][k]+d[k][j];//注意这里必须严格
}
}
}
int main(){
scanf("%d%d",&n,&m);
memset(a,0x3f,sizeof a);
for(int i=1;i<=n;i++)a[i][i]=0;
for(int i=1;i<=m;i++){
int u,v,w;
scanf("%d%d%d",&u,&v,&w);
a[u][v]=a[v][u]=min(a[u][v],w);
}
memcpy(d,a,sizeof a);
floyd();
if(sum==0x3f3f3f3f)puts("No solution.");
else{
int k=n+1,p=n+1;
for(int i=1;i<=num;i++)if(ans[i]
有向图的最小环更简单,我们枚举起点 s = 1 ∼ n s=1\sim n s=1∼n,对此进行单源最短路径算法,并且第一次取出s对节点进行更新之后把d[s]改为无穷大,当s第二次被取出的时候这个长度就是最小环长度
给定一张由 T 条边构成的无向图,点的编号为 1∼1000 之间的整数。
求从起点 S 到终点 E 恰好经过 N 条边(可以重复经过)的最短路。
2≤T≤100,
2≤N≤106
注意: 数据保证一定有解。
发现边很少,于是我们将点离散化,使用邻接矩阵存图
定义矩阵运算:设矩阵A,B,定义A*B=C,其中 C [ i ] [ j ] = min k = 1 n { A [ i ] [ k ] + B [ k ] [ j ] } C[i][j]=\min_{k=1}^n\lbrace A[i][k]+B[k][j] \rbrace C[i][j]=mink=1n{A[i][k]+B[k][j]}
若矩阵A就是我们存图的邻接矩阵,那么 A m [ i ] [ j ] A^m[i][j] Am[i][j]就是从i到j的经过m条边的最短路,于是我们使用快速幂即可得到答案,因为这个新矩阵乘法很明显具备结合律
#include
#include
#include
三个最短路算法必须掌握
然后就是经典问题
1.次短路及路径方案统计
2.最小环问题
3.传递闭包问题
经典思想
1.分层图
2.二分答案转判定
3.拆分路径统计答案
4.拆图分别计算
定义:在无向图 G = ( V , E ) G=(V,E) G=(V,E)中,一颗连接所有节点的树(无根树)被称为这个无向图的生成树,其中边权之和最小的那一颗被称为最小生成树
定理:最小生成树一定包含无向图中权值最小的边
证明:
反证法,假设无向图G=(V,E)存在一颗最小生成树不包含权值最小的边,把这条边加入最小生成树集合之后会形成一个环,这个环上任意一条边都比新加入的边的权值大,任意断开一条可以得到一个权值和更小的生成树,假设不成立,故命题成立
推论:
给定一张无向图 G = ( V , E ) , n = ∣ V ∣ , m = ∣ E ∣ G=(V,E),n=|V|,m=|E| G=(V,E),n=∣V∣,m=∣E∣从 E E E中选出 k < n − 1 k
K r u s k a l Kruskal Kruskal算法就是基于上述推论的。 K r u s k a l Kruskal Kruskal算法总是维护无向图的最小生成森林,最初可以认为生成森林是由零条边构成,每个节点各自构成一颗仅包含一个节点的树
在任意时刻 K r u s k a l Kruskal Kruskal算法从剩余的边中选出一条权值最小的,并把这条边的两个端点属于的生成树连通,图中节点的连接情况可以用并查集维护
详细的说,步骤如下
struct node{
int u,v,w;
bool operator<(const node b)const{
w
P r i m Prim Prim同样基于推论,与 K r u s k a l Kruskal Kruskal不同的是, p r i m prim prim算法始终维护最小生成树的一部分,不断扩大这个部分,最终扩展为一颗最小生成树
我们设 S S S表示我们维护的最小生成树集合, T T T表示剩余节点集合,我们每一次需要找到一条边,两个端点分属两个集合且边权最小,然后将这条边累计上答案,属于 T T T的那个端点加入到 S S S中,直到 ∣ S ∣ = ∣ V ∣ |S|=|V| ∣S∣=∣V∣
在具体实现中,最初 S S S中只有一号节点,我们维护一个数组 d d d,对于每个节点 x x x,若 x ∈ S x\in S x∈S,则 d [ x ] d[x] d[x]表示 x x x加入 S S S时所连接的最小边的权值,若 x ∈ T x\in T x∈T,则 d [ x ] d[x] d[x]表示 x x x与 S S S中节点相连的最小边权
类比 D i j k s t r a Dijkstra Dijkstra算法,我们也对其进行更新操作,用数组 v i s vis vis标记节点是否属于 S S S,然后找到不属于 S S S的节点的 d d d值最小的,将其加入 S S S,并且累计答案,然后这个节点对其所有出边且属于 T T T的节点进行更新操作,直到所有节点更新完毕
时间复杂度为 O ( n 2 ) O(n^2) O(n2)可以使用二叉堆优化为 O ( m log n ) O(m\log n ) O(mlogn),但这就不如 K r u s k a l Kruskal Kruskal算法了,代码复杂度和常数复杂度都是
由于 p r i m prim prim算法的时间复杂度仅与节点个数有关,所以常用于稠密图尤其是完全图的最小生成树求解
int prim(){
memset(d,0x3f,sizeof d);
memset(vis,0,sizeof vis);
d[1]=0;
for(int i=1;id[j]))num=j;
}
vis[num]=1;
for(int j=1;j<=n;j++){
if(!vis[j])d[j]=min(d[j],a[num][j]);
}
}
for(int i=2;i<=n;i++)ans+=d[i];
return ans;
}
因为 d d d数组的定义,于是 ∑ i = 1 n d [ i ] \sum_{i=1}^nd[i] ∑i=1nd[i]就是答案
国防部(DND)希望通过无线网络连接几个北部前哨站。
在建立网络时将使用两种不同的通信技术:每个前哨站都有一个无线电收发器,一些前哨站还有一个通信卫星。
任意两个拥有通信卫星的前哨站不论它们的位置如何,都可以通过卫星进行通信。
而如果利用无线电进行通信,则需要两个前哨站的距离不能超过 D D D 方可进行通信。
而 D D D 的大小取决于收发器的功率,收发器的功率越大, D D D 也就越大,但是需要的成本也就越高。
出于采购和维护的考虑,所有的前哨站都采用相同的收发器,也就是说所有前哨站的无线电通信距离 D D D 都是相同的。
你需要确定在保证任意两个前哨站之间都能进行通信(直接或间接)的情况下, D D D 的最小值是多少。
输入格式
第一行包含整数 N N N,表示共有 N N N 组测试数据。
每组数据的第一行包含两个整数 S S S 和 P P P,其中 S S S 为卫星个数, P P P 为前哨站个数。
接下来 P P P 行每行包含两个整数 x x x 和 y y y,分别表示一个前哨站的横纵坐标。
输出格式
输出一个实数,表示 D D D 的最小值,结果保留两位小数。
数据范围
1 ≤ S ≤ 100 , 1≤S≤100, 1≤S≤100,
S ≤ P ≤ 500 , S≤P≤500, S≤P≤500,
0 ≤ x , y ≤ 10000 0≤x,y≤10000 0≤x,y≤10000
分析:
首先这道题是一个完全图,我们需要对每一个前哨站互相连边
算法1:二分答案
单调性很容易发现, D D D更大肯定比 D D D更小使得可以通信的前哨站更多,于是我们可以二分答案,设当前二分的值是 m i d mid mid,我们将边权大于 m i d mid mid的边赋值为 1 1 1,小于 m i d mid mid的边赋值为0,很明显,我们假设所有边权为 0 0 0的边所连接到的点构成的若干个连通块,而我们的卫星可以使得连通块相连,即对每一个连通块安装一个卫星,就可以连通整张图,于是我们只需要统计连通块的个数,与 S S S相比较即可确定二分上下界变化,对于连通块的求解需要使用并查集维护
const int N = 510;
const double eps = 1e-4;
int n, m, x[N], y[N], fa[N];
double d[N][N];
int findfa(int x) { return x == fa[x] ? x : fa[x] = findfa(fa[x]); }
bool check(double now) {
for (int i = 1; i <= n; i++) fa[i] = i;
for (int i = 1; i < n; i++)
for (int j = i + 1; j <= n; j++)
if (d[i][j] <= now) fa[findfa(i)] = findfa(j);
int sum = 0;
for (int i = 1; i <= n; i++)
if (fa[i] == i) sum++;
return sum <= m;
}
int main() {
int tt; cin >> tt;
while (tt--) {
cin >> m >> n;
for (int i = 1; i <= n; i++)
scanf("%d%d", &x[i], &y[i]);
for (int i = 1; i < n; i++)
for (int j = i + 1; j <= n; j++)
d[i][j] = (double)sqrt((x[i] - x[j]) * (x[i] - x[j]) + (y[i] - y[j]) * (y[i] - y[j]));
double l = 0.0, r = 14145.0, mid;
while (l + eps < r) {
mid = (l + r) / 2;
if (check(mid)) r = mid;
else l = mid;
}
printf("%.2lf\n", r);
}
return 0;
}
算法2: K r u s k a l Kruskal Kruskal
由上文推论,在本题中,我们实质上需要维护最小生成树的一个子图,满足子图上最大边的边权不超过 D D D,由推论性质得到,我们最小生成树有 P − 1 P-1 P−1条边,而卫星可以使得 S − 1 S-1 S−1条边不计入,于是我们只需要统计最小生成树里第 P − 1 − ( S − 1 ) = P − S P-1-(S-1)=P-S P−1−(S−1)=P−S大的边的边权就是答案
int p,s,f[100005],cnt,vis[100005],m;
struct node{
int len,u,v;
bool operator<(const node b)const{
return lenin[100005];
int get(pair a,pair b){
return (a.first-b.first)*(a.first-b.first)+(a.second-b.second)*(a.second-b.second);
}
int find(int x){
return x==f[x]?x:f[x]=find(f[x]);
}
void init(){
m=0;
scanf("%d%d",&s,&p);
for(int i=1;i<=p;i++)scanf("%d%d",&in[i].first,&in[i].second);
for(int i=1;i<=p;i++){
for(int j=i+1;j<=p;j++){
a[++m]={get(in[i],in[j]),i,j};
}
}
sort(a+1,a+m+1);
}
int Kruskal(){
memset(vis,0,sizeof vis);
for(int i=1;i<=p;i++)f[i]=i;
cnt=0;
for(int i=1;i<=m;i++){
int u=a[i].u,v=a[i].v;
u=find(u),v=find(v);
if(v==u)continue;
if(++cnt==p-s)return a[i].len;
f[u]=v;
}
}
int main(){
freopen("1.in","r",stdin);
int n;
scanf("%d",&n);
while(n--){
init();
printf("%.2f",sqrt(Kruskal()));
// for(int i=1;i<=m;i++)printf("%.3f ",sqrt(a[i].len));
puts("");
}
}
算法1的时间复杂度是 O ( n 2 log V ) O(n^2\log V) O(n2logV),算法2的时间复杂度是 O ( n 2 log n ) O(n^2\log n) O(n2logn),理论上讲算法2更优
这道题启发我们:
1.发掘题目单调性,使用二分判定答案
2.考虑Kruskal和prim思想的本质,理解最小生成树定理
简单的说题意,就是求最优比率生成树,即每条边有成本 c o s t cost cost和长度 l e n len len,要求 ∑ c o s t ∑ l e n \frac{\sum cost}{\sum len} ∑len∑cost的最小值
很明显,这是一道0/1分数规划与最小生成树的综合题
我们只需要建立一张图,每个边的权值是 c o s t i − m i d × l e n i cost_i-mid\times len_i costi−mid×leni,对这张图求最小生成树,若边权和非负0则令 l = m i d l=mid l=mid,否则令 r = m i d r=mid r=mid
为什么是最小生成树而不是最大生成树呢?因为只有取最小生成树的时候,我们满足这张图其余生成树的答案一定不小于最小生成树的比率,否则就可以继续减小(这里蓝书上出错了)
double cost[1005][1005];int n,vis[1005];
double d[1005][1005],dis[1005];
double b[1005][1005];
struct node{
int x,y,h;
}a[1005];
double get(node a,node b){
return sqrt((a.x-b.x)*(a.x-b.x)+(a.y-b.y)*(a.y-b.y));
}
void init(){
for(int i=1;i<=n;i++){
for(int j=i;j<=n;j++){
cost[i][j]=cost[j][i]=get(a[i],a[j]);
d[i][j]=d[j][i]=abs(a[i].h-a[j].h);
}
}
}
bool check(double mid){
// printf("%.5f\n",mid);
for(int i=1;i<=n;i++){
for(int j=i;j<=n;j++){
if(i==j){
b[i][j]=2e9;
continue;
}
b[i][j]=b[j][i]=d[i][j]-mid*cost[i][j];
// printf("%.5f ",b[i][j]);
}
// puts("");
}
memset(vis,0,sizeof vis);
for(int i=1;i<=n;i++)dis[i]=2e9;
dis[1]=0;
for(int i=1;idis[j])))id=j,now=dis[j];
}
// printf("%d\n",id);
vis[id]=1;
for(int j=1;j<=n;j++){
if(!vis[j]){
// printf("A%d %.4f ",j,dis[j]);
dis[j]=min(dis[j],b[id][j]);
// printf("%.4f\n",dis[j]);
}
}
}
double ans=0;
for(int i=2;i<=n;i++)ans+=dis[i];
//printf("%.3f\n",ans);
return ans>=0;
}
int main(){
//freopen("10.in","r",stdin);
while(~scanf("%d",&n)&&n){
for(int i=1;i<=n;i++){
scanf("%d%d%d",&a[i].x,&a[i].y,&a[i].h);
}
init();
double l=0,r=50;
while(r-l>1e-8){
double mid=(l+r)/2;
if(check(mid))l=mid;
else r=mid;
}
printf("%.3f\n",(l+r)/2);
}
}
这道题是最优比率生成树模型,非常重要
在顺利攻破 L o r d l s p Lordlsp Lordlsp 的防线之后, l q r lqr lqr 一行人来到了 L o r d l s p Lordlsp Lordlsp 的城堡下方。
L o r d l s p Lord lsp Lordlsp 黑化之后虽然拥有了强大的超能力,能够用意念力制造建筑物,但是智商水平却没怎么增加。
现在 l q r lqr lqr 已经搞清楚黑暗城堡有 N N N 个房间, M M M 条可以制造的双向通道,以及每条通道的长度。
l q r lqr lqr 深知 L o r d l s p Lord lsp Lordlsp 的想法,为了避免每次都要琢磨两个房间之间的最短路径, L o r d l s p Lord lsp Lordlsp 一定会把城堡修建成树形的。
但是,为了尽量提高自己的移动效率, L o r d l s p Lord lsp Lordlsp 一定会使得城堡满足下面的条件:
设 D [ i ] D[i] D[i] 为如果所有的通道都被修建,第 i i i 号房间与第 1 1 1 号房间的最短路径长度;而 S [ i ] S[i] S[i] 为实际修建的树形城堡中第 i i i 号房间与第 1 1 1 号房间的路径长度;要求对于所有整数 i i i,有 S [ i ] = D [ i ] S[i]=D[i] S[i]=D[i] 成立 。
为了打败 L o r d l s p Lord lsp Lordlsp, l q r lqr lqr 想知道有多少种不同的城堡修建方案。
你需要输出答案对 2 3 1 – 1 2^31–1 231–1 取模之后的结果。
输入格式
第一行有两个整数 N N N 和 M M M。
之后 M M M 行,每行三个整数 X X X, Y Y Y 和 L L L,表示可以修建 X X X 和 Y Y Y 之间的一条长度为 L L L 的通道。
输出格式
一个整数,表示答案对 231–1 取模之后的结果。
数据范围
2 ≤ N ≤ 1000 , 2≤N≤1000, 2≤N≤1000,
N − 1 ≤ M ≤ N ( N − 1 ) / 2 , N−1≤M≤N(N−1)/2, N−1≤M≤N(N−1)/2,
1 ≤ L ≤ 100 1≤L≤100 1≤L≤100
简单来说,本题让我们统计在一张无向图里有多少最短路径生成树,最短路径生成树是指对于任意边 ( u , v , w ) (u,v,w) (u,v,w)都有 D [ u ] + u = D [ v ] D[u]+u=D[v] D[u]+u=D[v]
我们可以先使用 D i j k s t r a Dijkstra Dijkstra求出D数组,然后统计 ∀ v ∈ [ 1 , n ] \forall v\in[1,n] ∀v∈[1,n]有多少个 u u u满足 D [ u ] + w u , v = D [ v ] D[u]+w_{u,v}=D[v] D[u]+wu,v=D[v],记为 c n t v cnt_v cntv,最后把所有的cnt乘起来即可,至于原因,因为正权图的原因,对于cnt的计算可以使用对D从小到大排序之后按顺序统计,由于数据较小我就直接上 n 2 n^2 n2了其实是我没调好
正确性证明:很明显,当 u u u满足 D [ u ] + w u , v = D [ v ] D[u]+w_{u,v}=D[v] D[u]+wu,v=D[v],此时从v连接所有的u都不会影响最短路径生成树,由乘法原理即可得到,这是无后效性的,因为最终n个节点必定会连通
#define x first
#define y second
#define mp make_pair
#define int long long
#define p ((1ll<<31)-1)
priority_queue >q;
paira[100005];
int head[10005],nxt[1000005],ver[1000005],cost[1000005],d[1000005],vis[1000005],tot,n,m,cnt[100050];
void add(int u,int v,int w){
nxt[++tot]=head[u],ver[tot]=v,cost[tot]=w,head[u]=tot;
}
void dijkstra(){
q.push(mp(0,1));
memset(d,0x3f,sizeof d);
d[1]=0;
while(q.size()){
int u=q.top().y,w=d[u];q.pop();
if(vis[u])continue;
vis[u]=1;
for(int i=head[u];i;i=nxt[i]){
int v=ver[i],z=cost[i];
if(d[v]>w+z){
d[v]=w+z;
q.push(mp(-d[v],v));
}
}
}
//for(int i=1;i<=n;i++)printf("%d ",d[i]);
//puts("");
}
int prim(){
for(int u=1;u<=n;u++){
for(int i=head[u];i;i=nxt[i]){
int v=ver[i];
if(d[u]+cost[i]==d[v])cnt[v]++;
}
}
int ans=1;
for(int i=1;i<=n;i++)if(cnt[i])ans=ans*cnt[i]%p;
return ans;
}
signed main(){
// freopen("castle8.in","r",stdin);
scanf("%lld%lld",&n,&m);
for(int i=1;i<=m;i++){
int u,v,w;
scanf("%lld%lld%lld",&u,&v,&w);
add(u,v,w);
add(v,u,w);
}
dijkstra();
printf("%lld\n",prim());
}
这道题是关于最短路径生成树模型的,并且求方案数,结论务必理解记忆
一群小丑演员,以其出色的柔术表演,可以无限量的钻进同一辆汽车中,而闻名世界。
现在他们想要去公园玩耍,但是他们的经费非常紧缺。
他们将乘车前往公园,为了减少花费,他们决定选择一种合理的乘车方式,可以使得他们去往公园需要的所有汽车行驶的总公里数最少。
为此,他们愿意通过很多人挤在同一辆车的方式,来减少汽车行驶的总花销。
由此,他们可以很多人驾车到某一个兄弟的家里,然后所有人都钻进一辆车里,再继续前进。
公园的停车场能停放的车的数量有限,而且因为公园有入场费,所以一旦一辆车子进入到公园内,就必须停在那里,不能再去接其他人。
现在请你想出一种方法,可以使得他们全都到达公园的情况下,所有汽车行驶的总路程最少。
第一行包含整数 n,表示人和人之间或人和公园之间的道路的总数量。
接下来 n 行,每行包含两个字符串 A、B 和一个整数 L,用以描述人 A 和人 B 之前存在道路,路长为 L,或者描述某人和公园之间存在道路,路长为 L。
道路都是双向的,并且人数不超过 20,表示人的名字的字符串长度不超过 10,公园用 Park 表示。
再接下来一行,包含整数 s,表示公园的最大停车数量。
你可以假设每个人的家都有一条通往公园的道路。
简述题意,其实就是说,求图上的一颗生成树,使得1号节点的入度不超过S的情况下权值和最小
这里介绍一种并不常用的最小生成树算法:消圈算法
简单来说,就是在无向图上随便指定一颗生成树,然后考虑剩下的边,加入一个就会产生环,然后把环上最大的边断开,这样重复操作直到加入的边比环上最大边还要大为止,这样就得到了最小生成树,只不过由于程序实现比较复杂,复杂度也并不突出,于是就很少用(最古老的最小生成树算法)
对于本题,我们需要使用这种思想
首先,我们将1号节点与其他节点的边断开,这样我们就得到了若干个连通块,对每一个连通块分别求最小生成树,设有T个连通块,若T>S本题无解,因为不可能连通
然后我们尝试对于一个连通块T_i,T_i中所有与1有边的节点,我们选择最小的那一个进行连接,这样我们就连通了这个连通块,以此类推,我们就得到了原图的一棵生成树
接着我们就使用消圈算法的思想,尝试改动S-T条边使得答案更优,由于每个连通块内部再无需改动了,因为连通块内部求了最小生成树,于是我们的改动也只与1号节点有关
我们考虑与1号节点有边但边没有选入生成树的节点,将其加入生成树后势必会形成一个环,在环上找最大的边(假设这个点是x,则我们需要找到的最大的边就是原生成树上1->x的最大边)进行断开,就得到了一个权值和更小的生成树,重复这个步骤直到改动完S-T条边或者改动的边无法加入生成树为止,这个步骤需要我们维护1号节点在生成树上到每个节点所经过的边的边权最大值,可以使用深度优先遍历维护
int cnt,m,s,deg,ans,a[32][32],d[32],lst[32],vis[32],c[32],t[32][32],ver[32],p,f[32],mxx[32],mxy[32];
//t数组维护目前的最小生成树森林,f、mxx、mxy维护的是1->x的路径上的最大边
mapH;
void prim(int rt){//求连通块内部最小生成树,使用prim更好处理
d[rt]=0;
for(int i=1;i<=p;i++){
int u=0;
for(int j=1;j<=p;j++)
if(!vis[ver[j]] &&(u==0||d[ver[j]]a[u][v])
d[v]=a[u][v],lst[v]=u;
}
}
int maxx=rt;
for(int i=1;i<=p;i++){
int u=ver[i];
if(rt==u)continue;
ans+=d[u];
t[lst[u]][u]=t[u][lst[u]]=d[u];
if(a[1][u]t[u][v])
f[v]=f[u],mxx[v]=mxx[u],mxy[v]=mxy[u];
else
f[v]=t[u][v],mxx[v]=u,mxy[v]=v;
dp(v);
}
vis[u]=0;
}
bool solve(){//执行最后的消圈过程
int mn=1<<30,id;
for(int i=2;i<=cnt;i++){
if(t[1][i]!=0x3f3f3f3f||a[1][i]==0x3f3f3f3f)continue;
if(a[1][i]-t[mxx[i]][mxy[i]]=0)return 0;
ans+=mn,t[1][id]=t[id][1]=a[1][id];
t[mxx[id]][mxy[id]]=t[mxy[id]][mxx[id]]=0x3f3f3f3f;
f[id]=a[1][id],mxx[id]=1,mxy[id]=id;
vis[1]=1;
dp(id);
return 1;
}
int main(){
H["Park"]=1;cnt=1;//注意P是大写的(调试2h就因为这个)
scanf("%d",&m);
memset(a,0x3f,sizeof(a));
for(int i=1;i<=m;i++){
int u,v,w;
char x[15],y[15];
scanf("%s%s%d",x,y,&w);
if(!H[x])H[x]=++cnt;
if(!H[y])H[y]=++cnt;
u=H[x],v=H[y];
a[u][v]=a[v][u]=min(a[u][v], w);
}
scanf("%d",&s);
Prim();
memset(vis,0,sizeof(vis));
dp(1);
while(deg
此题启发我们消圈思想,拆图计算,合并消圈
题目描述
给定一张无向图,结点和边均有权值。所有结点权值之和为 0,点权可以沿边传递,传递点权的代价为边的权值。求让所有结点权值化为 0 的最小代价。
解法
容易想到本题与最小生成树有关。一种不难想出的思路是求出原图的最小生成树,将最小生成树上所有边的权值之和作为答案。
但经过思考,可以发现这样得到的不一定是最优解。首先,原图可能并不联通;其次,可以将原图划分为若干个点权之和均为 0 的子图,在这些子图中分别转移点权,最后将答案合并。这样得到的方案或许会更优。
此时我们发现划分方案不止一种,如何确定最终的方案成了需要解决的最大问题。
注意到本题中 N N N 范围较小,允许我们把所有点权和为 0 的子图(以下简称“合法子图”)的最小生成树全部求出。因此可以先枚举原图点集的所有子集,对于每个点权和为 0 的点集,用这些点和连接它们的边构造一张合法子图。我们能够轻易求出这些合法子图的最小生成树。但有些合法子图或许并不联通,为避免对之后的求解造成影响,需要把这些子图的最小生成树边权和设为 ∞ ∞ ∞.
接下来需要把这些子图中的若干个合并起来,得到全局最优解。与划分的情形相同,合并这些子图的方案也有多种。可以使用 D P DP DP 得到最优解。
具体地,考虑进行类似背包的 D P DP DP,将每个合法子图视作可以放入背包的一个物品。设 A 、 B A、B A、B为两个不同合法子图的点集,合法子图的最小生成树边权和为 S S S,可以写出如下状态转移方程:
f A ∪ B = min { f A ∪ B , f A + S B } , A ∩ B = ⊘ f_{A∪B}=\min\lbrace f_{A∪B},f_A+S_B\rbrace,A∩B=⊘ fA∪B=min{fA∪B,fA+SB},A∩B=⊘
最终 f 2 n − 1 f_{2^n-1} f2n−1 即为所求的答案。至此,本题得到解决。
int n,m,a[20],fa[20],s[1<<16],f[1<<16],ans[1<<16];
struct node{
int u,v,w;
bool operator <(const node b)const{
return w
这题是一道动态规划与最小生成树的综合题,主要启发我们缜密思考题目,学会把题目转化为图论模型
总的来说,今天这一节需要掌握的知识点有
定义:设 d i s [ i , j ] dis[i,j] dis[i,j]表示 i , j i,j i,j在树中的距离,则树的直径( d i a m e t e r diameter diameter,本文简记 d i a dia dia) d i a = d i s [ u , v ] ( ∀ i , j , d i s [ i , j ] ≤ d i s [ u , v ] ) dia=dis[u,v](\forall i,j,dis[i,j]\le dis[u,v]) dia=dis[u,v](∀i,j,dis[i,j]≤dis[u,v]),通俗的讲,树的直径是树中最长的一条链
性质:
树的直径的求法
1.DP法,使用较少,掌握BFS就可以了
void dp(int x) {
v[x] = 1;
for (int i = head[x]; i; i = Next[i]) {
int y = ver[i];
if (v[y]) continue;
dp(y);
ans = max(ans, d[x] + d[y] + edge[i]);
d[x] = max(d[x], d[y] + edge[i]);
}
}
2.DFS/BFS法
概述,我们先任选一个节点,假设1,求出所有节点到1的距离d,然后找到d值最大的节点p,再求出所有节点到p的距离d1,d1值最大的节点q,p->q的路径就是树的直径
证明:我们p节点就是树的最深的一端,然后以p为根再求出q,我们就相当于求出了树的两个最深的端点,连起来就是答案,证明限于篇幅,感性理解就行
对于这个求d数组的过程,使用DFS/BFS都可以,一般来讲BFS已经足够,DFS就略去,反正也一样的道理
tot=1;
int bfs(int t){
memset(d,-1,sizeof d);
d[t]=0;
queueq;q.push(t);
while(q.size()){
int u=q.front();q.pop();
for(int i=head[u];i;i=nxt[i]){
int v=ver[i];
if(d[v]!=-1)continue;
lst[v]=i;
d[v]=d[u]+cost[i];
q.push(v);
}
}
int p=-1;
for(int i=1;i<=n;i++)if(p==-1||s[p]
设 T = ( V , E , W ) T=(V,E,W) T=(V,E,W) 是一个无圈且连通的无向图(也称为无根树),每条边都有正整数的权,我们称 T T T 为树网(treenetwork
),其中 V V V, E E E 分别表示结点与边的集合, W W W 表示各边长度的集合,并设 T T T 有 n n n 个结点。
路径:树网中任何两结点 a a a, b b b 都存在唯一的一条简单路径,用 d ( a , b ) d(a, b) d(a,b) 表示以 a , b a, b a,b 为端点的路径的长度,它是该路径上各边长度之和。我们称
d ( a , b ) d(a, b) d(a,b) 为 a , b a, b a,b 两结点间的距离。
D ( v , P ) = min { d ( v , u ) } D(v, P)=\min\{d(v, u)\} D(v,P)=min{d(v,u)}, u u u 为路径 P P P 上的结点。
树网的直径:树网中最长的路径成为树网的直径。对于给定的树网 T T T,直径不一定是唯一的,但可以证明:各直径的中点(不一定恰好是某个结点,可能在某条边的内部)是唯一的,我们称该点为树网的中心。
偏心距 E C C ( F ) \mathrm{ECC}(F) ECC(F):树网 T T T 中距路径 F F F 最远的结点到路径 F F F 的距离,即
E C C ( F ) = max { D ( v , F ) , v ∈ V } \mathrm{ECC}(F)=\max\{D(v, F),v \in V\} ECC(F)=max{D(v,F),v∈V}
任务:对于给定的树网 T = ( V , E , W ) T=(V, E, W) T=(V,E,W) 和非负整数 s s s,求一个路径 F F F,他是某直径上的一段路径(该路径两端均为树网中的结点),其长度不超过 s s s(可以等于 s s s),使偏心距 E C C ( F ) \mathrm{ECC}(F) ECC(F) 最小。我们称这个路径为树网 T = ( V , E , W ) T=(V, E, W) T=(V,E,W) 的核(Core
)。必要时, F F F 可以退化为某个结点。一般来说,在上述定义下,核不一定只有一个,但最小偏心距是唯一的。
下面的图给出了树网的一个实例。图中, A − B A-B A−B 与 A − C A-C A−C 是两条直径,长度均为 20 20 20。点 W W W 是树网的中心, E F EF EF 边的长度为 5 5 5。如果指定 s = 11 s=11 s=11,则树网的核为路径DEFG
(也可以取为路径DEF
),偏心距为 8 8 8。如果指定 s = 0 s=0 s=0(或 s = 1 s=1 s=1、 s = 2 s=2 s=2),则树网的核为结点 F F F,偏心距为 12 12 12。
共 n n n 行。
第 1 1 1 行,两个正整数 n n n 和 s s s,中间用一个空格隔开。其中 n n n 为树网结点的个数, s s s 为树网的核的长度的上界。设结点编号以此为 1 , 2 … , n 1,2\dots,n 1,2…,n。
从第 2 2 2 行到第 n n n 行,每行给出 3 3 3 个用空格隔开的正整数 u , v , w u, v, w u,v,w,依次表示每一条边的两个端点编号和长度。例如,2 4 7
表示连接结点 2 2 2 与 4 4 4 的边的长度为 7 7 7。
一个非负整数,为指定意义下的最小偏心距。
仔细观察题目,我们可以得到如下性质
max 1 ≤ i ≤ j ≤ t , d i s ( i , j ) ≤ s ( max i ≤ k ≤ j { u k } , d i s ( u 1 , u i ) , d i s ( u j , u t ) ) \max_{1\le i\le j\le t,dis(i,j)\le s}\left(\max_{i\le k \le j}\lbrace u_k\rbrace,dis(u_1,u_i),dis(u_j,u_t)\right) 1≤i≤j≤t,dis(i,j)≤smax(i≤k≤jmax{uk},dis(u1,ui),dis(uj,ut))
由于 i ≤ j , i , j i\le j,i,j i≤j,i,j都是单调递增的,满足使用单调队列优化的性质,于是我们采用单调队列对式子 max i ≤ k ≤ j { u k } \max_{i\le k\le j}\lbrace u_k \rbrace maxi≤k≤j{uk}进行优化,就可以做到 O ( n ) O(n) O(n)
但我们还可以进一步进行优化,由于直径的最长性,任何从直径上的点 u i , u j u_i,u_j ui,uj分叉出去的子树,对于 u j u_j uj的距离不可能比 d i s ( u i , u j ) dis(u_i,u_j) dis(ui,uj)更大,否则直径就该换得了,于是 max i ≤ k ≤ j { u k } 可化为 max 1 ≤ k ≤ t { u k } \max_{i\le k\le j}\lbrace u_k \rbrace可化为\max_{1\le k\le t}\lbrace u_k \rbrace maxi≤k≤j{uk}可化为max1≤k≤t{uk},于是原式可变为
max 1 ≤ i ≤ j ≤ t , d i s ( i , j ) ≤ s ( max 1 ≤ k ≤ t { u k } , d i s ( u 1 , u i ) , d i s ( u j , u t ) ) \max_{1\le i\le j\le t,dis(i,j)\le s}\left(\max_{1\le k\le t}\lbrace u_k \rbrace,dis(u_1,u_i),dis(u_j,u_t)\right) 1≤i≤j≤t,dis(i,j)≤smax(1≤k≤tmax{uk},dis(u1,ui),dis(uj,ut))
至于 i , j i,j i,j的枚举,采用双指针扫描法可以在 O ( n ) O(n) O(n)的复杂度搞出来
综上,我们得到了一个 O ( n ) O(n) O(n)的优秀算法(据说NOIP原题数据范围 n ≤ 300 n\le 300 n≤300)
int ver[1000005],nxt[1000005],head[500005],cost[1000005],tot=1,d[1000005],f[1000005],pre[1000005],vis[1000005],n,s,mx=0xcfcfcfcf,ans=0x3f3f3f3f;
int t[1000005],num,sum[1000005],b[1000005],a[1000005];
void add(int u,int v,int w){
nxt[++tot]=head[u],ver[tot]=v,cost[tot]=w,head[u]=tot;
}
queuep;
int bfs(int s){
memset(d,-1,sizeof d);
p.push(s);d[s]=0;
while(p.size()){
int u=p.front();p.pop();
for(int i=head[u];i;i=nxt[i]){
int v=ver[i];
if(d[v]!=-1)continue;
d[v]=d[u]+cost[i];
pre[v]=i;
p.push(v);
}
}
int q=-1;
for(int i=1;i<=n;i++)if(q==-1||d[q]
小Q最近学习了一些图论知识。根据课本,有如下定义。树:无回路且连通的无向图,每条边都有正整数的权值来表示其长度。如果一棵树有 N N N个节点,可以证明其有且仅有 N − 1 N-1 N−1 条边。
路径:一棵树上,任意两个节点之间最多有一条简单路径。我们用 d i s ( a , b ) dis(a,b) dis(a,b)表示点 a a a和点 b b b的路径上各边长度之和。称 d i s ( a , b ) dis(a,b) dis(a,b)为 a 、 b a、b a、b两个节点间的距离。
直径:一棵树上,最长的路径为树的直径。树的直径可能不是唯一的。
现在小Q想知道,对于给定的一棵树,其直径的长度是多少,以及有多少条边满足所有的直径都经过该边。
第一行包含一个整数N,表示节点数。 接下来N-1行,每行三个整数a, b, c ,表示点 a和点b之间有一条长度为c的无向边。
共两行。第一行一个整数,表示直径的长度。第二行一个整数,表示被所有直径经过的边的数量。
我们上文提到的树的性质,第一问就不说了,板子。此题实际上就是让我们求必须边的数量,由必须边的性质:由所有的必须边组成一条链
于是我们的问题变成了如何在直径上找到这样一条链
直接寻找必须边比较复杂,我们可以采用容斥思想,找到所有的非必须边
设这棵树的点集为 V V V,我们定义一条直径上的点为 u 1 , u 2 , … u t u_1,u_2,…u_t u1,u2,…ut,组成集合 D D D,设 d [ u i ] d[u_i] d[ui]表示点 u i u_i ui不经过直径上的其他点所能在树中达到的最远距离,即 d [ u i ] = max k ∈ ( V − D ) ( d i s ( u i , k ) ) d[u_i]=\max_{k\in(V-D)}(dis(u_i,k)) d[ui]=maxk∈(V−D)(dis(ui,k))
关于d的求法,与上题的dfs函数无二,复杂度 O ( n ) O(n) O(n)
那么显而易见的,一条链 u i − > u j ( i < j ) u_i->u_j(i
于是我们可以用两个指针 l , r l,r l,r分别从两端扫描,最后 r − l r-l r−l就是答案
#include
using namespace std;
#define int long long
int vis[2000005],lst[2000005],d[2000005],s[2000005],b[5000005],cnt,c[5000005],n;
int head[2000005],ver[5000005],nxt[5000005],cost[5000005],tot=1,e[1000005];
void add(int u,int v,int w){
nxt[++tot]=head[u],ver[tot]=v,cost[tot]=w,head[u]=tot;
}
int bfs(int t){
memset(s,-1,sizeof s);
s[t]=0;
queueq;q.push(t);
while(q.size()){
int u=q.front();q.pop();
for(int i=head[u];i;i=nxt[i]){
int v=ver[i];
if(s[v]!=-1)continue;
lst[v]=i;
s[v]=s[u]+cost[i];
q.push(v);
}
}
int p=-1;
for(int i=1;i<=n;i++)if(p==-1||s[p]0;i--){
if(cur==d[b[i]])r=i;
if(i>0)cur+=cost[lst[b[i]]];
}
return r-l;
}
signed main(){
//freopen("dia1.in","r",stdin);
scanf("%lld",&n);
for(int i=1;i
定义:对于节点 x , y x,y x,y,若节点 z z z既是 x x x的祖先,也是 y y y的祖先,则称 z z z是 x , y x,y x,y的公共祖先, L C A ( x , y ) LCA(x,y) LCA(x,y)就是 x , y x,y x,y的公共祖先里深度最大的那一个
性质: L C A ( x , y ) LCA(x,y) LCA(x,y)是 x x x到 y y y的简单路径上深度最小的节点
求 L C A LCA LCA的方法有很多,朴素就不说了
思路:先预处理链之类的,然后对于两个点不断跳链直到跳到一条链上,此时深度较小的节点就是LCA
struct edge{
int to,ne;
}e[1000005];
int n,m,s,ecnt,head[500005],dep[500005],siz[500005],son[500005],top[500005],f[500005];
void add(int x,int y){
e[++ecnt].to=y;
e[ecnt].ne=head[x];
head[x]=ecnt;
}
void dfs1(int x){
siz[x]=1;
dep[x]=dep[f[x]]+1;
for(int i=head[x];i;i=e[i].ne){
int dd=e[i].to;
if(dd==f[x])continue;
f[dd]=x;
dfs1(dd);
siz[x]+=siz[dd];
if(!son[x]||siz[son[x]]=dep[top[y]])x=f[top[x]];
else y=f[top[y]];
}
printf("%d\n",dep[x]
使用欧拉序,欧拉序是指在深度优先遍历整棵树的是时候,节点刚递归进入的时候标记,退出的时候再标记,这样就可以有一个性质,节点 u u u的子树全部在序列两个u之间,于是我们就可以把倍增里左边的x和右边的y所构成的区间里求深度最小的节点,这个节点就是LCA(x,y)
设 f [ i ] [ k ] f[i][k] f[i][k]表示节点 i i i的 2 k 2^k 2k级祖先,有 f [ i ] [ k ] = f [ f [ i ] [ k − 1 ] ] [ k − 1 ] f[i][k]=f[f[i][k-1]][k-1] f[i][k]=f[f[i][k−1]][k−1],初值 f [ i ] [ 0 ] = f a [ i ] f[i][0]=fa[i] f[i][0]=fa[i]
DP的顺序我们需要知道 i i i节点的所有祖先,即 i i i的所有祖先都被 D P DP DP后再 D P i DPi DPi节点,这种以深度为顺序的,就是广度优先遍历,于是我们可以使用 B F S BFS BFS来实现这个 D P DP DP过程
queueq;
void bfs(int rt){
q.push(rt);
dep[rt]=1;
while(q.size()){
int u=q.front();q.pop();
for(int i=head[u];i;i=nxt[i]){
int v=ver[i];
if(dep[v])continue;
dep[v]=dep[u]+1;
f[v][0]=u;
for(int i=1;i<=t;i++){//t=log2(n)向上取整的结果
f[v][i]=f[f[v][i-1]][i-1];
}
q.push(v);
}
}
}
然后对于查询过程,我们先将 x , y x,y x,y调整至同一深度,这里默认最初 d e p [ y ] > d e p [ x ] dep[y]>dep[x] dep[y]>dep[x],即我们将y不断上拉,直到 d e p [ y ] = d e p [ x ] dep[y]=dep[x] dep[y]=dep[x]此时有两种情况,即 x x x本就是 y y y的祖先,此时 x = y x=y x=y直接返回即可,或者是我们同时将两个节点向上拉
int lca(int x,int y){
if(dep[x]>dep[y])swap(x,y);
for(int i=t;i>=0;i--)if(dep[f[y][i]]>=dep[x])y=f[y][i];
if(x==y)return x;
for(int i=t;i>=0;i--)if(f[x][i]!=f[y][i])x=f[x][i],y=f[y][i];
return f[x][0];
}
前言:stO tarjan Orz,tarjan是真牛
时间复杂度: O ( n + m ) O(n+m) O(n+m)
在任意时刻,深度优先遍历的节点分为三类
对于一个正在访问的节点 x x x,它的祖先的标记一定都是1,那么对于一个已经访问完成的节点 y y y, y y y向上走,走到的第一个标记为1的节点就是 L C A ( x , y ) LCA(x,y) LCA(x,y)
对于这个过程,我们可以使用并查集进行优化,当一个节点 x x x获得了2的标记时,我们将 y y y和 f a ( y ) fa(y) fa(y)合并为一个集合(此时 f a ( y ) fa(y) fa(y)肯定是集合的代表元,且 f a ( y ) fa(y) fa(y)的标记为1)
这样我们执行 f i n d ( y ) find(y) find(y)查询代表元的时候,实际上就是查找到了第一个 y y y的祖先中标记为1的节点,即 L C A ( x , y ) LCA(x,y) LCA(x,y)
// Tarjan算法离线求LCA (模板题:HDOJ2586)
const int SIZE = 50010;
int ver[2 * SIZE], Next[2 * SIZE], edge[2 * SIZE], head[SIZE];
int fa[SIZE], d[SIZE], v[SIZE], lca[SIZE], ans[SIZE];
vector query[SIZE], query_id[SIZE];
int T, n, m, tot, t;
void add(int x, int y, int z) {
ver[++tot] = y; edge[tot] = z; Next[tot] = head[x]; head[x] = tot;
}
void add_query(int x, int y, int id) {
query[x].push_back(y), query_id[x].push_back(id);
query[y].push_back(x), query_id[y].push_back(id);
}
int get(int x) {
if (x == fa[x]) return x;
return fa[x] = get(fa[x]);
}
void tarjan(int x) {
v[x] = 1;
for (int i = head[x]; i; i = Next[i]) {
int y = ver[i];
if (v[y]) continue;
d[y] = d[x] + edge[i];
tarjan(y);
fa[y] = x;
}
for (int i = 0; i < query[x].size(); i++) {
int y = query[x][i];
int id = query_id[x][i];
if (v[y] == 2) {
int lca = get(y);
ans[id] = min(ans[id], d[x] + d[y] - 2 * d[lca]);
}
}
v[x] = 2;
}
int main() {
cin >> T;
while (T--) {
cin >> n >> m;
for (int i = 1; i <= n; i++) {
head[i] = 0;
query[i].clear(), query_id[i].clear();
fa[i] = i, v[i] = 0;
}
tot = 0;
for (int i = 1; i < n; i++) {
int x, y, z;
scanf("%d%d%d", &x, &y, &z);
add(x, y, z), add(y, x, z);
}
for (int i = 1; i <= m; i++) {
int x, y;
scanf("%d%d", &x, &y);
if (x == y) ans[i] = 0;
else {
add_query(x, y, i);
ans[i] = 1 << 30;
}
}
tarjan(1);
for (int i = 1; i <= m; i++) printf("%d\n", ans[i]);
}
}
在前缀和与差分中,我们实现了序列上的区间修改,单点查询问题,现在我们要对这个思想运用到树中,实现 x − y x-y x−y的路径上修改,单点查询,这种操作被称为树上差分
原来的前缀和变成了子树和,区间操作对应路径操作
1.点权形式,将 u − > v u->v u−>v的路径上的点权全部加上 d d d,我们的操作是将 v a l [ x ] + = d , v a l [ y ] + = d , v a l [ L C A ( x , y ) ] − = d , v a l [ f a ( L C A ( x , y ) ) ] − = d val[x]+=d,val[y]+=d,val[LCA(x,y)]-=d,val[fa(LCA(x,y))]-=d val[x]+=d,val[y]+=d,val[LCA(x,y)]−=d,val[fa(LCA(x,y))]−=d
这种操作的本质是 L C A ( x , y ) LCA(x,y) LCA(x,y)也需要进行操作
2.边权形式,将 u − > v u->v u−>v的路径上的边权全部加上 d d d,我们的操作是将 v a l [ x ] + = d , v a l [ y ] + = d , v a l [ L C A ( x , y ) ] − = 2 ∗ d val[x]+=d,val[y]+=d,val[LCA(x,y)]-=2*d val[x]+=d,val[y]+=d,val[LCA(x,y)]−=2∗d
这种操作的本质是我们在操作的时候默认边权下放到了点权,这就使得 L C A ( u , v ) − > f a ( L C A ( u , v ) ) LCA(u,v)->fa(LCA(u,v)) LCA(u,v)−>fa(LCA(u,v))的边权下放到了 L C A ( u , v ) LCA(u,v) LCA(u,v),一个也不能统计到,于是需要减去两倍的 d d d
深绘里一直很讨厌雨天。
灼热的天气穿透了前半个夏天,后来一场大雨和随之而来的洪水,浇灭了一切。
虽然深绘里家乡的小村落对洪水有着顽固的抵抗力,但也倒了几座老房子,几棵老树被连根拔起,以及田地里的粮食被弄得一片狼藉。
无奈的深绘里和村民们只好等待救济粮来维生。
不过救济粮的发放方式很特别。首先村落里的一共有 n n n 座房屋,并形成一个树状结构。然后救济粮分 m m m 次发放,每次选择两个房屋 ( x , y ) (x,~y) (x, y),然后对于 x x x 到 y y y 的路径上(含 x x x 和 y y y)每座房子里发放一袋 z z z 类型的救济粮。
然后深绘里想知道,当所有的救济粮发放完毕后,每座房子里存放的最多的是哪种救济粮。
输入的第一行是两个用空格隔开的正整数,分别代表房屋的个数 n n n 和救济粮发放的次数 m m m。
第 2 2 2 到 第 n n n 行,每行有两个用空格隔开的整数 a , b a,~b a, b,代表存在一条连接房屋 a a a 和 b b b 的边。
第 ( n + 1 ) (n + 1) (n+1) 到第 ( n + m ) (n + m) (n+m) 行,每行有三个用空格隔开的整数 x , y , z x,~y,~z x, y, z,代表一次救济粮的发放是从 x x x 到 y y y 路径上的每栋房子发放了一袋 z z z 类型的救济粮。
输出 n n n 行,每行一个整数,第 i i i 行的整数代表 i i i 号房屋存放最多的救济粮的种类,如果有多种救济粮都是存放最多的,输出种类编号最小的一种。
如果某座房屋没有救济粮,则输出 0 0 0。
对于这道题,我们需要查询每个位置上的救济粮的最大值,于是我们就需要统计每个位置所有的救济粮数量,朴素的思想是开一个计数数组 a a a, a [ i , j ] a[i,j] a[i,j]表示节点i上存放节点j的数量,每一次在x到y的路径上朴素修改,我们就得到了一个时间复杂度 O ( n m ) O(nm) O(nm),空间复杂度 O ( n m ) O(nm) O(nm)的优秀算法。考虑进行优化
优化1. 树上路径操作可以使用树上差分,具体的我们对于一条指令 ( x , y , z ) (x,y,z) (x,y,z),将 a [ x , z ] + + , a [ y , z ] + + , a [ l c a ( x , y ) , z ] − − , a [ f a ( l c a ( x , y ) ) , z ] − − a[x,z]++,a[y,z]++,a[lca(x,y),z]--,a[fa(lca(x,y)),z]-- a[x,z]++,a[y,z]++,a[lca(x,y),z]−−,a[fa(lca(x,y)),z]−−,这样我们就成功地将修改复杂度降到了 O ( 1 ) O(1) O(1),但时间复杂度没有实际变化(合并查询的时候仍然需要 O ( n m ) O(nm) O(nm))
优化2. 针对优化1的继续优化,我们发现,合并查询的时候复杂度过高,而修改复杂度较低,我们就可以想办法均衡一下。这个均衡需要靠数据结构来实现,观察数据支持 O ( ( n + m ) log n ) O((n+m)\log n) O((n+m)logn),于是我们大胆猜测穿一个 log \log log级别的数据结构来维护,很明显,线段树合并算法登场
详细的说,我们为了节省空间,先对z进行离散化,然后对于每一个节点都开一颗线段树存储,注意线段树用动态开点,这样我们的空间复杂度就会降低到 O ( m log n ) O(m\log n) O(mlogn),原因是线段树上修改一个叶子节点需要 ⌈ log n ⌉ \lceil\log n\rceil ⌈logn⌉个节点,总共修改 O ( m ) O(m) O(m)次,并且线段树合并算法时间复杂度也只有 O ( n log n ) O(n\log n) O(nlogn)
int head[100050],ver[200500],nxt[200500],tot;//图
int f[100005][25],dep[100005];//LCA
int ans[100005],n,m,T,root[100005],zmx,a[100005],b[100005],cnt;
struct edge{
int x,y,z;
}que[100005];//question
struct node{
int lc,rc,id,mx;
}t[5000000];
#define ls t[x].lc
#define rs t[x].rc
int new_node(){
t[++cnt]={0,0,0,0};
return cnt;
}
void pushup(int x){
t[x].id=t[ls].id,t[x].mx=t[ls].mx;
if(t[x].mx>1;
if(xb<=mid){
if(!ls)ls=new_node();
update(L,mid,xb,d,ls);
}
else {
if(!rs)rs=new_node();
update(mid+1,R,xb,d,rs);
}
pushup(x);
}
int merge(int p,int q,int l,int r){
if(!p)return q;
if(!q)return p;
if(l==r){
t[p].mx=t[q].mx+t[p].mx;
return p;
}
int mid=l+r>>1;
t[p].lc=merge(t[p].lc,t[q].lc,l,mid);
t[p].rc=merge(t[p].rc,t[q].rc,mid+1,r);
pushup(p);
return p;
}
//以上线段树动态开点加合并
queueq;
void bfs(){
dep[1]=1;
q.push(1);
while(q.size()){
int u=q.front();q.pop();
for(int i=head[u];i;i=nxt[i]){
int v=ver[i];
if(dep[v])continue;
dep[v]=dep[u]+1;
f[v][0]=u;
for(int i=1;i<=T;i++)f[v][i]=f[f[v][i-1]][i-1];
q.push(v);
}
}
}
int lca(int x,int y){
if(dep[x]>dep[y])swap(x,y);
for(int i=T;i>=0;i--)if(dep[f[y][i]]>=dep[x])y=f[y][i];
if(x==y)return x;
for(int i=T;i>=0;i--)if(f[y][i]!=f[x][i])x=f[x][i],y=f[y][i];
return f[x][0];
}
//以上LCA
void dfs(int u){
for(int i=head[u];i;i=nxt[i]){
int v=ver[i];
if(dep[v]>dep[u]){
dfs(v);
root[u]=merge(root[u],root[v],1,zmx);
}
}
ans[u]=t[root[u]].mx?t[root[u]].id:0;
}
//以上统计答案
void change(int x,int y,int z){
int fa=lca(x,y);
update(1,zmx,z,1,root[x]);
update(1,zmx,z,1,root[y]);
update(1,zmx,z,-1,root[fa]);
if(f[fa][0])update(1,zmx,z,-1,root[f[fa][0]]);
}
//修改操作
void add(int u,int v){
nxt[++tot]=head[u];ver[tot]=v,head[u]=tot;
}
int main(){
// freopen("1.in","r",stdin);
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)root[i]=++cnt;
T=log(n)/log(2.0)+1;
for(int i=1;i
关于动态开点线段树合并时间复杂度的简要证明
我们可以发现,线段树合并的时间与两棵树重合的节点相关,即最坏情况下也不会大于小的那颗树的节点个数,类似于启发式合并,我们之前也证明了至多会创建 O ( m log n ) O(m\log n) O(mlogn)个节点,也即合并复杂度就是这个数
总时间复杂度为 O ( ( n + m ) log ( n + m ) ) O((n+m)\log (n+m)) O((n+m)log(n+m))
小c
同学认为跑步非常有趣,于是决定制作一款叫做《天天爱跑步》的游戏。《天天爱跑步》是一个养成类游戏,需要玩家每天按时上线,完成打卡任务。
这个游戏的地图可以看作一一棵包含 n n n 个结点和 n − 1 n-1 n−1 条边的树,每条边连接两个结点,且任意两个结点存在一条路径互相可达。树上结点编号为从 1 1 1 到 n n n 的连续正整数。
现在有 m m m 个玩家,第 i i i 个玩家的起点为 s i s_i si,终点为 t i t_i ti。每天打卡任务开始时,所有玩家在第 0 0 0 秒同时从自己的起点出发,以每秒跑一条边的速度,不间断地沿着最短路径向着自己的终点跑去,跑到终点后该玩家就算完成了打卡任务。 (由于地图是一棵树,所以每个人的路径是唯一的)
小c
想知道游戏的活跃度,所以在每个结点上都放置了一个观察员。在结点 j j j 的观察员会选择在第 w j w_j wj 秒观察玩家,一个玩家能被这个观察员观察到当且仅当该玩家在第 w j w_j wj 秒也正好到达了结点 j j j 。小c
想知道每个观察员会观察到多少人?
注意:我们认为一个玩家到达自己的终点后该玩家就会结束游戏,他不能等待一 段时间后再被观察员观察到。 即对于把结点 j j j 作为终点的玩家:若他在第 w j w_j wj 秒前到达终点,则在结点 j j j 的观察员不能观察到该玩家;若他正好在第 w j w_j wj 秒到达终点,则在结点 j j j 的观察员可以观察到这个玩家。
第一行有两个整数 n n n 和 m m m。其中 n n n 代表树的结点数量, 同时也是观察员的数量, m m m 代表玩家的数量。
接下来 n − 1 n-1 n−1 行每行两个整数 u u u 和 v v v,表示结点 u u u 到结点 v v v 有一条边。
接下来一行 n n n 个整数,其中第 j j j 个整数为 w j w_j wj , 表示结点 j j j 出现观察员的时间。
接下来 m m m 行,每行两个整数 s i s_i si,和 t i t_i ti,表示一个玩家的起点和终点。
对于所有的数据,保证 1 ≤ s i , t i ≤ n , 0 ≤ w j ≤ n 1\leq s_i,t_i\leq n, 0\leq w_j\leq n 1≤si,ti≤n,0≤wj≤n。
输出 1 1 1 行 n n n 个整数,第 j j j 个整数表示结点 j j j 的观察员可以观察到多少人。
首先 s i − t i s_i-t_i si−ti的路径上,观察员分为两类,一类是在 s i − l c a ( s i , t i ) s_i-lca(s_i,t_i) si−lca(si,ti)上的,一类是在 l c a ( s i , t i ) − t i lca(s_i,t_i)-t_i lca(si,ti)−ti上的
我们处理出所有点的深度记为 d d d
那么玩家 i i i能够被观察员 j j j观察到当且仅当满足以下两个条件之一
1.
d [ s i ] − d [ j ] = w j d[s_i]-d[j]=w_j d[si]−d[j]=wj
2.
d [ s i ] + d [ j ] − 2 × d [ l c a ( s i , t i ) ] = w j d[s_i]+d[j]-2\times d[lca(s_i,t_i)]=w_j d[si]+d[j]−2×d[lca(si,ti)]=wj
因为这两个条件具有互斥性,所以我们可以分开统计贡献,下面以统计满足条件1的节点数量
我们对条件一进行变式得到 d [ s i ] = d [ j ] + w j d[s_i]=d[j]+w_j d[si]=d[j]+wj,这样我们就分离了两个变量,那么下一步,我们尝试把它变成区间修改,类似上题雨天的尾巴,开一个计数数组 a [ i , j ] a[i,j] a[i,j](表示节点 i i i有 a [ i , j ] 个 j a[i,j]个j a[i,j]个j)判断,于是我们可以将 s i − l c a ( s i , t i ) s_i-lca(s_i,t_i) si−lca(si,ti)的路径上的点的 a [ u ] [ d [ s i ] ] ( u ∈ s i − l c a ( s i , t i ) ) + + a[u][d[s_i]](u\in s_i-lca(s_i,t_i))++ a[u][d[si]](u∈si−lca(si,ti))++,条件2类似,将 t i − l c a ( s i , t i ) t_i-lca(s_i,t_i) ti−lca(si,ti)上的点的 a [ u ] [ d [ s i ] − 2 × d [ l c a ( s i , t i ) ] ] ( u ∈ s i − l c a ( s i , t i ) ) + + a[u][d[s_i]-2\times d[lca(s_i,t_i)]](u\in s_i-lca(s_i,t_i))++ a[u][d[si]−2×d[lca(si,ti)]](u∈si−lca(si,ti))++,答案就是
∑ i = 1 n ( a [ i ] [ d [ i ] + w i ] + a [ i ] [ w i − d [ i ] ] ) \sum_{i=1}^n\left(a[i][d[i]+w_i]+a[i][w_i-d[i]]\right) i=1∑n(a[i][d[i]+wi]+a[i][wi−d[i]])
对于操作一样使用树上差分,设b为差分计数数组,则由于本题记录的是边权,于是对于路径 u − v u-v u−v我们只需要修改 u , v , l c a ( u , v ) u,v,lca(u,v) u,v,lca(u,v)三个节点即可,所以 a [ i , j ] = ∑ v ∈ s o n ( i ) b [ v ] [ j ] a[i,j]=\sum_{v\in son(i)}b[v][j] a[i,j]=∑v∈son(i)b[v][j],其中 s o n son son代表子树节点集合
但题目最大数据点 n , m ≤ 3 × 1 0 5 n,m\le 3\times 10^5 n,m≤3×105,采用上一题的线段树合并算法由于空间过大很容易 M L E MLE MLE掉,当然就算空间过了常数也不行,于是线段树,它死了启发我们需要一个更加高效的算法
我们发现,这道题具备区间减法性质,且每个点只问一个特殊值的数量,于是我们可以采用前缀和的思想方式,利用区间减法性质,进行“树上前缀和”
我们发现,由于我们采用树上差分的操作,使得我们在 b b b数组中至多只会有 O ( m ) O(m) O(m)个值有意义,其余的值是冗余操作,于是我们为了节省空间,可以在每一个节点开一个 v e c t o r vector vector,存 i , j i,j i,j表示在树中有 j j j个值为 i i i,这样我们达到了一定的时空平衡。每一次树上差分操作只需要在 v e c t o r vector vector后面插入一个节点即可,反正最多所有节点加起来也才 O ( m ) O(m) O(m),这个插入的节点j只可能为-1/1,避免了在 v e c t o r vector vector里再去查找 i i i,然后修改对应 j j j的操作
然后我们开一个全局的计数数组 c c c(一维), c i c_i ci代表目前值为 i i i的有 c i c_i ci个,我们可以通过一次深度优先遍历,在遍历到节点 x x x的时候,我们开两个辅助变量 c n t 1 , c n t 2 cnt1,cnt2 cnt1,cnt2,分别记录 c [ d [ x ] + w x ] , c [ w x − d [ x ] ] c[d[x]+w_x],c[w_x-d[x]] c[d[x]+wx],c[wx−d[x]]的值,之后我们就把当前节点的vector全部累加上c数组,继续遍历子节点,最后回溯的时候做一个类似前缀和的操作, a n s [ x ] = c [ d [ x ] + w x ] − c n t 1 + c [ w x − d [ x ] ] − c n t 2 ans[x]=c[d[x]+w_x]-cnt1+c[w_x-d[x]]-cnt2 ans[x]=c[d[x]+wx]−cnt1+c[wx−d[x]]−cnt2,我们就可以得到子树中 d [ x ] + w x , w x − d [ x ] d[x]+w_x,w_x-d[x] d[x]+wx,wx−d[x]的值的数量,注意 w x − d [ x ] w_x-d[x] wx−d[x]需要平移数组
总得来说,这题最后的统计答案具备区间减法性质,这个性质一样可以扩展到树上,也即一段区间信息能由其他两段信息推出,我们就可以采用类似前缀和的方式进行优化,而上一题是 max \max max操作,不满足区间减法性质,于是我们就不能够这样优化
const int MAXN = 6e5 + 10;
const int MAXM = 1e6 + 21000;
int n, m, cnt, s[MAXN], t[MAXN], lc[MAXN], lenth[MAXN];
int head[MAXN], dep[MAXN], fa[MAXN], son[MAXN], siz[MAXN], top[MAXN], val[MAXN], c[MAXN];
int nxt[MAXM], to[MAXM];
int start[MAXN], cntt[MAXM << 1], a[MAXN], len[MAXN], ans[MAXN];
vector anc[MAXN], tail[MAXN];
void add(int x, int y) {
cnt++;
nxt[cnt] = head[x];
head[x] = cnt;
to[cnt] = y;
}
void dfs1(int u, int fat) {
siz[u] = 1, fa[u] = fat, dep[u] = dep[fat] + 1;
for (int i = head[u]; i; i = nxt[i]) {
int v = to[i];
if (v != fa[u]) {
c[v] = c[u] + 1;
dfs1(v, u);
siz[u] += siz[v];
if (siz[son[u]] < siz[v])
son[u] = v;
}
}
}
void dfs2(int u, int tp) {
top[u] = tp;
if (!son[u])
return;
dfs2(son[u], tp);
for (int i = head[u]; i; i = nxt[i]) {
int v = to[i];
if (v != son[u] && v != fa[u]) {
dfs2(v, v);
}
}
}
int lca(int x, int y) {
while (top[x] != top[y]) {
if (dep[top[x]] < dep[top[y]])
swap(x, y);
x = fa[top[x]];
}
return dep[x] < dep[y] ? x : y;
}//树剖LCA
void dfs3(int u) {
int xx = cntt[dep[u] + val[u]], yy = cntt[val[u] - dep[u] + MAXN];
for (int i = head[u]; i; i = nxt[i]) {
int v = to[i];
if (v == fa[u])
continue;
dfs3(v);
}
cntt[dep[u]] += start[u];
for (int i = 0; i < tail[u].size(); i++) {
int v = tail[u][i];
cntt[lenth[v] - dep[t[v]] + MAXN]++;
}
ans[u] += cntt[dep[u] + val[u]] - xx + cntt[val[u] - dep[u] + MAXN] - yy;
for (int i = 0; i < anc[u].size(); i++) {
int v = anc[u][i];
cntt[dep[s[v]]]--, cntt[lenth[v] - dep[t[v]] + MAXN]--;
}
return;
}
int main() {
scanf("%d%d",&n,&m);
for (int i = 1; i < n; ++i) {
int x,y;
scanf("%d%d",&x,&y);
add(x, y), add(y, x);
}
for (int i = 1; i <= n; ++i)
scanf("%d",&val[i]);
dfs1(1, 0);
dfs2(1, 1);
for (int i = 1; i <= m; ++i) {
scanf("%d%d",&s[i],&t[i]);
lc[i] = lca(s[i], t[i]);
lenth[i] = c[s[i]] + c[t[i]] - c[lc[i]] * 2;
anc[lc[i]].push_back(i);
tail[t[i]].push_back(i);
start[s[i]]++;
if (dep[s[i]] == dep[lc[i]] + val[lc[i]])
ans[lc[i]]--;
}
dfs3(1);
for (int i = 1; i <= n; ++i)
cout << ans[i] << ' ';
return 0;
}
题意:给定一张无向连通图,求其严格次小生成树
n ≤ 1 0 5 , m ≤ 3 × 1 0 5 n\le 10^5,m\le 3\times 10^5 n≤105,m≤3×105
首先,常用思路是我们先求出最小生成树,然后尝试加边,毫无疑问,设我们对最小生成树加入一条边 ( u , v , w ) (u,v,w) (u,v,w)就会导致原最小生成树上 u − v u-v u−v的路径上会形成环,这时我们将环上断开一条边就得到了一个次小生成树的候选答案,下面我们思考应该断开怎样一条边
首先由最小生成树的性质,原 u − v u-v u−v路径上的任何一条边的边权都应该小于或等于 w w w,那么我们比较两条边,边权为 x 1 , x 2 x_1,x_2 x1,x2,设 ( x 1 < x 2 (x_1
但这样就完全正确了吗,注意,我们要求的是“严格”次小生成树,于是当图中 u − v u-v u−v最大边与 w w w相等的时候就无法使用,这启发我们再维护一个严格次大边
于是我们的问题就变成了如何求任意两点路径上的最大边权和严格次大边权。
考虑DP,朴素DP应该很容易写出状态转移方程,但复杂度无疑是 O ( n m ) O(nm) O(nm)的,这不可能完成 1 0 5 10^5 105级别的数据
于是我们考虑优化,这个朴素的DP似乎不具有使用数据结构优化的前提,于是我们就只剩下一种优化方法,倍增
因为本题的DP满足区间加法和可拼凑性,满足倍增优化DP的前提
我们设 f [ x , k ] f[x,k] f[x,k]表示 x x x的 2 k 2^k 2k倍祖先,这个可以 O ( n ) O(n) O(n)倍增预处理
那么我们设 g [ x , k , 0 / 1 ] g[x,k,0/1] g[x,k,0/1]表示 x x x到 x x x的 2 k 2^k 2k倍祖先的最大边权和严格次大边权, k = 0 k=0 k=0时最大边权是自己,次大边权为负无穷,则有
g [ x , k , 0 ] = m a x ( g [ x , k − 1 , 0 ] , g [ f [ x , k − 1 ] , k − 1 , 0 ] ) g[x,k,0]=max(g[x,k-1,0],g[f[x,k-1],k-1,0]) g[x,k,0]=max(g[x,k−1,0],g[f[x,k−1],k−1,0])
{ g [ x , k , 1 ] = max ( g [ f [ x , k − 1 ] , k − 1 , 0 ] , g [ x , k − 1 , 1 ] ) ( g [ x , k − 1 , 0 ] > g [ f [ x , k − 1 ] , k − 1 , 0 ] ) g [ x , k , 1 ] = max ( g [ x , k − 1 , 0 ] , g [ f [ x , k − 1 ] , k − 1 , 1 ] ) ( g [ x , k − 1 , 0 ] ) < g [ f [ x , k − 1 ] , k − 1 , 0 ] ) g [ x , k , 1 ] = max ( g [ x , k − 1 , 1 ] , g [ f [ x , k − 1 ] , k − 1 , 1 ] ) ( g [ x , k − 1 , 0 ] = g [ f [ x , k − 1 ] , k − 1 , 0 ] ) \left\{ \begin{aligned} g[x,k,1]&=\max(g[f[x,k-1],k-1,0],g[x,k-1,1])&(g[x,k-1,0]>g[f[x,k-1],k-1,0])\\ g[x,k,1]&=\max(g[x,k-1,0],g[f[x,k-1],k-1,1])&(g[x,k-1,0])
至于对于路径 u − v u-v u−v的最大边权和严格次大边权可以看作 l c a ( u , v ) lca(u,v) lca(u,v)与 u , v u,v u,v之间的连边按照处理 g g g数组的方式处理出来,然后合并两条路径也按照 g g g数组的处理方式来,就可以求出了,然后我们就可以 O ( log n ) O(\log n) O(logn)求出一个严格次小生成树的候选答案
时间复杂度
#include
using namespace std;
typedef long long ll;
const int N = 3e5 + 7, M = 3e5 + 7, MM = 3e5 + 7;
const ll INF = 0x7ffffffffff;;
int n, m;
ll sum;
int cnt, head[MM], ver[MM], nex[MM], edge[MM];
int tree[MM], pre[N], ppre[N][23], depth[N], lg[N];
ll maxf[N][23], minf[N][23];
struct E {
int from, to, w;
E() {}
E(int from, int to, int w) : from(from), to(to), w(w) {}
bool operator < (const E& b)const {
return w < b.w;
}
}e[M];
void add(int x, int y, int w) {
ver[++cnt] = y;
nex[cnt] = head[x];
edge[cnt] = w;
head[x] = cnt;
}
int find(int x) {
return x == pre[x] ? x : pre[x] = find(pre[x]);
}
void read() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= m; i++)
scanf("%d%d%d", &e[i].from, &e[i].to, &e[i].w);
for (int i = 0; i < N; i++)
pre[i] = i;
}
void work1() {
sort(e + 1, e + m + 1);
for (int i = 1; i <= m; i++) {
int x = e[i].from, y = e[i].to, w = e[i].w;
int fx = find(x), fy = find(y);
if (fx != fy) {
pre[fx] = fy;
sum += w;
add(x, y, w);
add(y, x, w);
tree[i] = 1;
}
}
}
void dfs(int f, int fa, int w) {
depth[f] = depth[fa] + 1;
ppre[f][0] = fa;
minf[f][0] = -INF;
maxf[f][0] = w;
for (int i = 1; (1 << i) <= depth[f]; i++) {
ppre[f][i] = ppre[ppre[f][i - 1]][i - 1];
maxf[f][i] = max(maxf[f][i - 1], maxf[ppre[f][i - 1]][i - 1]);
minf[f][i] = max(minf[f][i - 1], minf[ppre[f][i - 1]][i - 1]);//这里分清次小关系
if (maxf[f][i - 1] > maxf[ppre[f][i - 1]][i - 1]) minf[f][i] = max(minf[f][i], maxf[ppre[f][i - 1]][i - 1]);
else if (maxf[f][i - 1] < maxf[ppre[f][i - 1]][i - 1]) minf[f][i] = max(minf[f][i], maxf[f][i - 1]);
}
for (int i = head[f]; i; i = nex[i]) {
int y = ver[i], w = edge[i];
if (y != fa) {
dfs(y, f, w);
}
}
}
int lca(int x, int y) {
if (depth[x] < depth[y]) swap(x, y);
while (depth[x] > depth[y])
x = ppre[x][lg[depth[x] - depth[y]] - 1];
if (x == y) return x;
for (int i = lg[depth[x]] - 1; i >= 0; i--) {
if (ppre[x][i] != ppre[y][i])
x = ppre[x][i], y = ppre[y][i];
}
return ppre[x][0];
}
ll qmax(int x, int y, int maxx) {
ll ans = -INF;
for (int i = lg[depth[x]] - 1; i >= 0; i--) {
if (depth[ppre[x][i]] >= depth[y]) {
if (maxx != maxf[x][i]) ans = max(ans, maxf[x][i]);
else ans = max(ans, minf[x][i]);
x = ppre[x][i];
}
}
return ans;
}
void work2() {
for (int i = 1; i <= n; i++)
lg[i] = lg[i - 1] + (1 << lg[i - 1] == i);
dfs(1, 0, 0);
ll ans = INF;
for (int i = 1; i <= m; i++) {
if (tree[i]) continue;
int x = e[i].from, y = e[i].to, w = e[i].w;
int lc = lca(x, y);
ll maxx = qmax(x, lc, w);
ll maxv = qmax(y, lc, w);
ans = min(ans, sum - max(maxx, maxv) + w);
}
printf("%lld\n", ans);
}
int main() {
read();
work1();
work2();
return 0;
}
H 国有 n n n 个城市,这 n n n 个城市用 n − 1 n-1 n−1 条双向道路相互连通构成一棵树, 1 1 1 号城市是首都,也是树中的根节点。
H 国的首都爆发了一种危害性极高的传染病。当局为了控制疫情,不让疫情扩散到边境城市(叶子节点所表示的城市),决定动用军队在一些城市建立检查点,使得从首都到边境城市的每一条路径上都至少有一个检查点,边境城市也可以建立检查点。但特别要注意的是,首都是不能建立检查点的。
现在,在 H 国的一些城市中已经驻扎有军队,且一个城市可以驻扎多个军队。一支军队可以在有道路连接的城市间移动,并在除首都以外的任意一个城市建立检查点,且只能在一个城市建立检查点。一支军队经过一条道路从一个城市移动到另一个城市所需要的时间等于道路的长度(单位:小时)。
请问最少需要多少个小时才能控制疫情。注意:不同的军队可以同时移动。
本题很明显满足单调性。使用贪心思想,一个军队很明显靠根节点越近越好。所以我们考虑二分答案,贪心判定
设二分的值为 m i d mid mid,那么所有的军队分为两类
int cnt, tot, sum, n, m, dep[50005], gap[50005], ans, mid, head[50005], dist[50005][30], f[50005][30], number[50005], dis, tie[100006], tot2;bool edn[50005];
paircup[50005];
bool vis[50005];
struct node {
int v, nxt, w;
}e[1000005];
void add(int u, int v, int w) {
++cnt;
e[cnt].v = v, e[cnt].w = w, e[cnt].nxt = head[u], head[u] = cnt;
}
void bfs() {
queueq;
q.push(1);
int t = log2(n) + 1;
dep[1] = 1;
while (!q.empty()) {
int u = q.front(); q.pop();
for (int i = head[u]; i; i = e[i].nxt) {
int v = e[i].v;
if (dep[v])continue;
dep[v] = dep[u] + 1;
f[v][0] = u, dist[v][0] = e[i].w;
for (int j = 1; j <= t; j++) {
f[v][j] = f[f[v][j - 1]][j - 1];
dist[v][j] = dist[v][j - 1] + dist[f[v][j - 1]][j - 1];
}
q.push(v);
}
}
}
bool dfs(int u) {
bool vis2 = false;
if (edn[u])return 1;
for (int i = head[u]; i; i = e[i].nxt) {
int v = e[i].v;
if (dep[v] < dep[u])continue;
vis2 = true;
if (!dfs(v))return 0;
}
if (!vis2)return 0;
return 1;
}
bool check() {
memset(cup, 0, sizeof cup);
memset(gap, 0, sizeof gap);
memset(tie, 0, sizeof tie);
memset(vis, 0, sizeof vis);
memset(edn, 0, sizeof edn);
int t = log2(n);
int sum = 0;
tot = dis = tot2 = 0;
for (int i = 1; i <= m; i++) {
int u = number[i];
sum = 0;
for (int i = t; i >= 0; i--) {
if (f[u][i] > 1 && sum + dist[u][i] <= mid) {
sum += dist[u][i];
u = f[u][i];
}
}
if (f[u][0] == 1 && sum + dist[u][0] <= mid) {
cup[++tot].first = mid - (sum + dist[u][0]);
cup[tot].second = u;
}//还能走
else {
edn[u] = 1;//标记
}
}
for (int i = head[1]; i; i = e[i].nxt) {
int v = e[i].v;
if (!dfs(v)) {
vis[v] = 1;
}
}
sort(cup + 1, cup + tot + 1);
for (int i = 1; i <= tot; i++) {
int time = cup[i].first;
int u = cup[i].second;
if (vis[u] && dist[u][0] > time) {
vis[u] = 0;
}
else {
tie[++dis] = time;
}
}
for (int i = head[1]; i; i = e[i].nxt) {
if (vis[e[i].v])gap[++tot2] = dist[e[i].v][0];
}
if (dis < tot2)return false;
sort(tie + 1, tie + dis + 1);
sort(gap + 1, gap + tot2 + 1);
int l = 1, r = 1;//双指针扫描
while (l <= dis && r <= tot2) {
if (tie[l] >= gap[r]) {
l++, r++;
}
else {
l++;
}
}
if (r > tot2) {
return true;
}
return false;
}
int r;
int query() {
int l = 0;
while (l <= r) {
mid = l + r >> 1;
if (check())r = mid - 1;
else l = mid + 1;
}
return l;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n;
r = 0;
for (int i = 1; i < n; i++) {
int u,v , w;
cin >> u >> v >> w;
add(u, v, w);
add(v, u, w);
r += w;
}
bfs();
cin >> m;
for (int i = 1; i <= m; i++) {
cin >> number[i];
}
cout << query();
return 0;
}
好的下面让我们来总结本节要点
知识点:
众所周知,一颗树是由 N N N个点 N − 1 N-1 N−1条边组成的连通图,我们在树上任意加上一条边,树上就会产生一个环,这样 N N N个点 N N N条边组成的连通无向图就是基环树,当然,若不一定连通,这也可能是一个由基环树构成的森林,简称基环树森林
在有向图中,也有类似的概念, N N N个点 N N N条边,每个点有且仅有一条出边的连通有向图内部有一个环,而其他边就好像向内收缩的样子,我们称这种有向图为内向基环树
类似的,每个点有且仅有一条入边的有连通向图好像对向外扩展,这种有向图被称为外向基环树
若不保证连通,也有可能是内/外向树森林
[外链图片转存中…(img-heXIjT3P-1661348574934)])
对于基环树的结构虽然简单,但比一般的树要复杂一些,因此常常成为一些经典模型的扩展,如基环树直径,基环树上两点路径,基环树上动态规划……
对于解决基环树问题,我们一般是先找到树上的环,并且以环作为基环树的“广义根节点”,把除了环以外的节点作为若干颗子树进行处理,然后考虑和环有一起计算
无论如何,基环树找环基本是必备操作,但我们上面提到的三种基环树,找环的方法也不尽相同
先看第一个普通基环树找环的过程:
void get(int u,int v,int z){
sum[1]=z;//像下文说的求sum数组那样,先把断开的这条边的权值加上
while(v!=u){
h[++cnt]=v;
sum[cnt+1]=cost[lst[v]];
v=ver[lst[v]^1];
}//凭借标记不断回跳
h[++cnt]=u;
for(int i=1; i <= cnt; i++){
vis[h[i]]=true;
sum[i]+=sum[i-1];
}
}
void dfs(int u){
dfn[u]=++num;
for(int i=head[u];i;i=nxt[i]){
int v=ver[i];
if(!dfn[v]){
lst[v]=i;
dfs(v);
}
else if(i^1!=lst[u]&&dfn[v]>dfn[u])get(u,v,cost[i]);
/*对环判定的解释:dfn是dfs序,之所以以i^1!=lst[u]&&dfn[v]>dfn[u]作为环的判断条件,是因为
1.i^1!=lst[u]是在特判父亲节点,因为无向图的成对存储
2.需要dfn[v]>dfn[u]是强制要求环在u的子树内出现,如此便不会重复统计
*/
}
}
对于内向基环树的环可以使用拓扑排序求得,当然拓扑排序本身就是有向图判环的绝招
scanf("%d%d",&n,&m);
for(int i=1;i <= n;i++){
scanf("%d",&f[i]);
in[f[i]]++;//i->f[i]
}
for(int i=1;i<=n;i++)
if(!in[i])q.push(i);
while(q.size()){
int x=q.front();
q.pop();
if(!--in[f[x]])q.push(f[x]);
}
对于外向基环树,我们发现,拓扑排序不行,因为每一个点都有且仅有1的入度,于是我们使用跳父亲的方式,即不断往上跳,因为每一个节点都有父亲,于是我们肯定可以跳到环上,然后就得到了环
while(!vis[fa[rt]])vis[fa[rt]]=1,rt=fa[rt];
u=rt,v=fa[rt];
while(u!=v){
s[++num]=v;
v=fa[v];
}
s[++num]=u;
下面我们讨论几个经典模型在基环树上的扩展
同样的,基环树的直径是基环树上最长的链,下面我们讨论它的求法
很明显,基环树的直径有两种可能,一是在环上某个节点的子树上,一是跨环的直径
对于第一种情况,我们只需要找出环,然后将环上节点标记,对每一个环上的节点的子树上进行一般的求直径的方法更新答案即可,我们设环上节点为 s [ i ] s[i] s[i],一共有 c n t cnt cnt个节点
对于第二种情况,我们需要先 d f s dfs dfs找出环上每个节点作为一端的在自己子树上的最长链,这个通过 d f s / b f s dfs/bfs dfs/bfs可以 O ( n ) O(n) O(n)求出,记作 d [ s [ i ] ] d[s[i]] d[s[i]]
然后第二步,我们对环上的节点做前缀和,记作 s u m [ i ] sum[i] sum[i],也即 s u m [ i ] = c o s t [ s [ 1 ] , s [ c n t ] ] + ∑ j = 2 i c o s t [ s [ j − 1 ] , s [ j ] ] sum[i]=cost[s[1],s[cnt]]+\sum_{j=2}^{i}cost[s[j-1],s[j]] sum[i]=cost[s[1],s[cnt]]+∑j=2icost[s[j−1],s[j]],于是我们需要解决的问题就变成了:找到一对 i , j i,j i,j,使得 d [ s [ i ] ] + d [ s [ j ] ] + m a x ( s u m [ c n t ] − a b s ( s u m [ s [ i ] ] − s u m [ s [ j ] ] ) , a b s ( s u m [ s [ i ] ] − s u m [ s [ j ] ] ) ) d[s[i]]+d[s[j]]+max(sum[cnt]-abs(sum[s[i]]-sum[s[j]]),abs(sum[s[i]]-sum[s[j]])) d[s[i]]+d[s[j]]+max(sum[cnt]−abs(sum[s[i]]−sum[s[j]]),abs(sum[s[i]]−sum[s[j]])),我们之所以在处理前缀和的时候直接加上了 1 − c n t 1-cnt 1−cnt的边的边权,是因为我们需要做前缀和,这个并不影响答案并且更好处理,具体原因看代码实现并自己手玩一下就可以理解了
至于这个找 i , j i,j i,j的过程,我们可以将环复制一倍变成链来处理,直接进行DP,因为我们将环变成了两倍,于是我们就不必考虑 max \max max函数,这是因为在最优化DP中,我们只需要考虑最优解是否在决策集合内,即 i , j i,j i,j,在环中会出现为 i + c n t , j + c n t , i , j i+cnt,j+cnt,i,j i+cnt,j+cnt,i,j四个,若我们设定 i < j i
max j < i , i − j < c n t { s u m [ s [ i ] ] − s u m [ s [ j ] ] + d [ s [ i ] ] + d [ s [ j ] ] } \max_{jj<i,i−j<cntmax{sum[s[i]]−sum[s[j]]+d[s[i]]+d[s[j]]}
对于这个式子,我们可以采用单调队列优化,将 s u m [ s [ i ] ] + d [ s [ i ] ] sum[s[i]]+d[s[i]] sum[s[i]]+d[s[i]]提出来,使用单调队列维护单调递增的 d [ s [ j ] ] − s u m [ s [ j ] ] d[s[j]]-sum[s[j]] d[s[j]]−sum[s[j]],直接 O ( c n t ) O(cnt) O(cnt)DP更新答案即可
我们梳理一下这个过程
#include
using namespace std;
const int N=1000005;
int ver[N<<1],cost[N<<1],nxt[N<<1],head[N],tot=1,n,cnt,num,dfn[N],lst[N],h[N<<1],q[N<<1],vis[N];
long long d[N],sum[N<<1],ans,Ans;
void add(int u,int v,int w){
nxt[++tot]=head[u],ver[tot]=v,cost[tot]=w,head[u]=tot;
}
void get(int u,int v,int z){
sum[1]=z;
while(v!=u){
h[++cnt]=v;
sum[cnt+1]=cost[lst[v]];
v=ver[lst[v]^1];
}
h[++cnt]=u;
for(int i=1; i <= cnt; i++){
vis[h[i]]=true;
h[cnt+i]=h[i];
sum[cnt+i]=sum[i];
}
for(int i=1;i<=cnt+cnt;i++)sum[i]+=sum[i-1];
}//得到环之后的预处理
void dfs(int u){
dfn[u]=++num;
for(int i=head[u];i;i=nxt[i]){
int v=ver[i];
if(!dfn[v]){
lst[v]=i;
dfs(v);
}
else if(i^1!=lst[u]&&dfn[v]>dfn[u])get(u,v,cost[i]);
}
}//处理找环
void dp(int u){
vis[u]=true;
for(int i=head[u];i;i=nxt[i]){
int v=ver[i];
if(!vis[v]){
dp(v);
ans=max(ans,d[u]+d[v]+cost[i]);
d[u]=max(d[u],d[v]+cost[i]);
}
}
}//d数组就是需要的最长链
int main(){
scanf("%d",&n);
for(int u=1;u<=n;u++){
int v,w;
scanf("%d%d",&v,&w);
add(u,v,w);add(v,u,w);
}
for(int u=1;u<=n;u++)
if(!dfn[u]){
cnt=0;ans=0;
dfs(u);
for(int i=1;i<=cnt;i++)dp(h[i]);
int l=1,r=0;
for(int i=1;i<=cnt<<1;i++){
while(l<=r&&q[l]<=i-cnt)l++;
if(l<=r)ans=max(ans,d[h[i]]+d[h[q[l]]]+sum[i]-sum[q[l]]);
while(l<=r&&d[h[q[r]]]-sum[q[r]]<=d[h[i]]-sum[i])r--;
q[++r]=i;
}//单调队列统计答案
Ans+=ans;
}
printf("%lld",Ans);
}
为了随时与 r a i n b o w rainbow rainbow 快速交流, F r e d a Freda Freda 制造了两部传呼机。
F r e d a Freda Freda 和 r a i n b o w rainbow rainbow 所在的地方有 N N N 座房屋、 M M M 条双向光缆。
每条光缆连接两座房屋,传呼机发出的信号只能沿着光缆传递,并且传呼机的信号从光缆的其中一端传递到另一端需要花费 t t t 单位时间。
现在 F r e d a Freda Freda 要进行 Q Q Q 次试验,每次选取两座房屋,并想知道传呼机的信号在这两座房屋之间传递至少需要多长时间。
N N N 座房屋通过光缆一定是连通的,并且这 M 条光缆有以下三类连接情况:
A A A:光缆不形成环,也就是光缆仅有 N − 1 N−1 N−1条。
B B B:光缆只形成一个环,也就是光缆仅有 N N N 条。
C C C:每条光缆仅在一个环中。
请你帮帮他们。
输入格式
第一行包含三个用空格隔开的整数, N 、 M N、M N、M 和 Q Q Q。
接下来 M M M 行每行三个整数 x 、 y 、 t x、y、t x、y、t,表示房屋 x x x 和 y y y 之间有一条传递时间为 t t t 的光缆。
最后 Q Q Q 行每行两个整数 x 、 y x、y x、y,表示 F r e d a Freda Freda 想知道在 x x x 和 y y y之间传呼最少需要多长时间。
输出格式
输出 Q Q Q 行,每行一个整数,表示 F r e d a Freda Freda 每次试验的结果。
数据范围
2 ≤ N ≤ 10000 , 2≤N≤10000, 2≤N≤10000,
N − 1 ≤ M ≤ 12000 , N−1≤M≤12000, N−1≤M≤12000,
Q = 10000 , Q=10000, Q=10000,
1 ≤ x , y ≤ N , 1≤x,y≤N, 1≤x,y≤N,
1 ≤ t < 32768 1≤t<32768 1≤t<32768
30 30% 30的数据, M = N − 1 M=N-1 M=N−1
50 50% 50的数据, M = N M=N M=N
20 20% 20的数据, M > N M>N M>N
看到本题会发现这是一个全源最短路径问题,嗯此时你想到了 F l o y d Floyd Floyd,对不起,你只有 5 p t s 5pts 5pts
我们先来考虑 M = N − 1 M=N-1 M=N−1的数据,很明显 N N N个村庄构成一颗无根树,我们任选一个根节点进行 d f s dfs dfs,求出 d d d数组,其中 d [ i ] d[i] d[i]表示节点 i i i与根节点的距离,我们再倍增/树剖处理出 L C A LCA LCA,对于一个询问 u , v u,v u,v,答案就是 d [ u ] + d [ v ] − 2 × d [ l c a ( u , v ) ] d[u]+d[v]-2\times d[lca(u,v)] d[u]+d[v]−2×d[lca(u,v)]
再来考虑 N = M N=M N=M的数据,此时的询问有两种可能性
d [ u ] + d [ v ] + m i n ( s u m [ c n t ] − ∣ s u m [ r a n [ b [ u ] ] ] − s u m [ r a n [ b [ v ] ] ] ∣ , ∣ s u m [ r a n [ b [ u ] ] ] − s u m [ r a n [ b [ v ] ] ] ∣ ) d[u]+d[v]+min(sum[cnt]-|sum[ran[b[u]]]-sum[ran[b[v]]]|,|sum[ran[b[u]]]-sum[ran[b[v]]]|) d[u]+d[v]+min(sum[cnt]−∣sum[ran[b[u]]]−sum[ran[b[v]]]∣,∣sum[ran[b[u]]]−sum[ran[b[v]]]∣)
第三种情况超出了讨论范围,涉及到数据结构仙人掌树,也即圆方树
#define int long long
int s[1000005],num,head[1000005],ver[1000005],nxt[2000005],cost[2000005],tot=1,cnt,f[1000005][25];
int dep[1000005],b[1000005],d[1000005],sum[1000005],t,vis[1000005],dfn[1000006],lst[1000006],n,m,ran[1000005];
void add(int u,int v,int w){
nxt[++tot]=head[u],ver[tot]=v,cost[tot]=w,head[u]=tot;
}
void bfs(int s){
queueq;
q.push(s);
b[s]=s;
d[s]=0,dep[s]=1;
while(!q.empty()){
int u=q.front();q.pop();
for(int i=head[u];i;i=nxt[i]){
int v=ver[i];
if(dep[v]||vis[v])continue;
dep[v]=dep[u]+1;
b[v]=s;
d[v]=d[u]+cost[i];
f[v][0]=u;
for(int i=1;i<=t;i++)f[v][i]=f[f[v][i-1]][i-1];
q.push(v);
}
}
}//处理倍增LCA,d,b,dep
int lca(int x,int y){
if(dep[x]>dep[y])swap(x,y);
for(int i=t;i>=0;--i)if(dep[f[y][i]]>=dep[x])y=f[y][i];
if(x==y)return x;
for(int i=t;i>=0;--i)if(f[y][i]!=f[x][i])x=f[x][i],y=f[y][i];
return f[x][0];
}
void get(int u,int v,int z){
sum[1]=z;
while(u!=v){
s[++cnt]=v;
ran[v]=cnt;
sum[cnt+1]=cost[lst[v]];
v=ver[lst[v]^1];
}
s[++cnt]=u;
ran[u]=cnt;
for(int i=1;i<=cnt;i++){
sum[i]+=sum[i-1];
vis[s[i]]=1;
}
}//找环,注意ran数组建立映射
void dfs(int u){
dfn[u]=++num;
for(int i=head[u];i;i=nxt[i]){
int v=ver[i];
if(!dfn[v]){
lst[v]=i;
dfs(v);
}
else if(i^1!=lst[u]&&dfn[v]>dfn[u])get(u,v,cost[i]);
}
}
void init(){
if(n-1==m){
bfs(1);
return ;
}
dfs(1);
for(int i=1;i<=cnt;i++)bfs(s[i]);
}
int solve(int u,int v){
if(n-1==m||b[u]==b[v]){
return d[u]+d[v]-2*d[lca(u,v)];
}
else {
int ans=d[u]+d[v];
return ans+min(sum[cnt]-abs(sum[ran[b[u]]]-sum[ran[b[v]]]),abs(sum[ran[b[u]]]-sum[ran[b[v]]]));//注意这里的计算唉
}
}
signed main(){
// freopen("communicate9.in","r",stdin);
int q;
scanf("%lld%lld%lld",&n,&m,&q);
t=log(n)/log(2)+1;
for(int i=1;i<=m;i++){
int u,v,w;
scanf("%lld%lld%lld",&u,&v,&w);
add(u,v,w);
add(v,u,w);
}
init();
while(q--){
int u,v;
scanf("%lld%lld",&u,&v);
printf("%lld\n",solve(u,v));
}
}
//这份代码已经可以获得80%的分数了
给定一个 n n n 个顶点的有向图,每个顶点有且仅有一条出边。
对于顶点 i i i,记它的出边为 ( i , a [ i ] ) (i,a[i]) (i,a[i])。
再给出 q q q 组询问,每组询问由两个顶点 a 、 b a、b a、b组成,要求输出满足下面条件的 x 、 y x、y x、y:
从顶点 a a a 沿着出边走 x x x 步和从顶点 b b b 沿着出边走 y y y 步后到达的顶点相同。
在满足条件 1 1 1 的情况下,如果解不唯一,则还需要令 max ( x , y ) \max(x,y) max(x,y) 最小。
在满足条件 1 1 1 和 2 2 2 的情况下,如果解不唯一,则还需要令 min ( x , y ) \min(x,y) min(x,y) 最小。
在满足条件 1 、 2 1、2 1、2 和 3 3 3 的情况下,如果解不唯一,则还需要令 x ≥ y x≥y x≥y。
如果不存在满足条件 1 1 1 的 x 、 y x、y x、y,输出 − 1 − 1 -1 -1 −1−1。
输入格式
第一行两个正整数 n n n 和 q q q。
第二行 n 个正整数 a [ 1 ] , a [ 2 ] , … , a [ n ] a[1],a[2],…,a[n] a[1],a[2],…,a[n]。
下面 q 行,每行两个正整数 a,b,表示一组询问。
输出格式
输出 q 行,每行两个整数。
数据范围
n , q ≤ 500000 , n,q≤500000, n,q≤500000,
a [ i ] ≤ n , a[i]≤n, a[i]≤n,
a , b ≤ n a,b\le n a,b≤n
本题的图是一个外向树森林,于是我们可以使用拓扑排序找环,然后我们分开讨论几种情况下的答案
#define pe pair
#define x first
#define y second
pe cmp(pe a,pe b){
if(max(a.x,a.y)max(b.x,b.y))return b;
if(min(a.x,a.y)min(b.x,b.y))return b;
return a.x >= a.y ? a : b;
}
//主函数中的调用:
pe a,b;
int sx=s[id[x]],sy=s[id[y]],now=num[pos[id[x]]];//跳至环上节点sx,sy并得到这个环的大小now,我们直接将两个决策存为a,b再比较谁优秀
a.x=dep[x]+(sy-sx+now)%now;//代码小技巧:abs(sy-sx)%now=(sy-sx+now)%now,前提:sy-sx>=-now
a.y=dep[y];
b.x=dep[x];
b.y=dep[y]+(sx-sy+now)%now;
pe ans=cmp(a,b);
于是我们很容易的就可以完成这道题
流程如下:
#include
#define pe pair
#define x first
#define y second
using namespace std;
const int N=500006;
int n,m,t,f[N][20],in[N],pos[N],cnt,num[N],s[N],dep[N],id[N];
vectore[N];
queueq;
void bfs(){//处理各个环上节点的子树信息,如归属id,dep等
for(int i=1;i<=n;i++)
if(pos[i]){
id[i]=i;
q.push(i);
}
else e[f[i][0]].push_back(i);
while(q.size()){
int x=q.front();
q.pop();
for(int i=0;idep[y])swap(x,y);
for(int i=t;i>=0;i--)if(dep[f[y][i]]>=dep[x])y=f[y][i];
if(x==y)return x;
for(int i=t;i>=0;i--)if(f[x][i]!=f[y][i])x=f[x][i],y=f[y][i];
return f[x][0];
}
pe cmp(pe a,pe b){
if(max(a.x,a.y)max(b.x,b.y))return b;
if(min(a.x,a.y)min(b.x,b.y))return b;
return a.x >= a.y ? a : b;
}
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i <= n;i++){
scanf("%d",&f[i][0]);
in[f[i][0]]++;
}
t=log(n)/log(2);
for(int i=1;i<=t;i++)
for(int x=1;x<=n;x++)
f[x][i]=f[f[x][i-1]][i-1];//直接处理倍增数组
for(int i=1;i<=n;i++)
if(!in[i])q.push(i);
while(q.size()){
int x=q.front();
q.pop();
if(!--in[f[x][0]])q.push(f[x][0]);
}//拓扑排序
for(int i=1;i<=n;i++)
if(in[i]&&!pos[i]){//拓扑排序之后还有入度的节点就是环上的节点,此时处理连通块
++cnt;
for(int j=i;!pos[j];j=f[j][0]){//标记整个环
pos[j]=cnt;
s[j]=++num[cnt];
}
}
//处理连通块
bfs();
while(m--){
int x,y;
scanf("%d %d",&x,&y);
if(pos[id[x]] != pos[id[y]])puts("-1 -1");
else if(id[x]==id[y]){
int p=lca(x,y);
printf("%d %d\n",dep[x]-dep[p],dep[y]-dep[p]);
}
else{
pe a,b;
int sx=s[id[x]],sy=s[id[y]],now=num[pos[id[x]]];
a.x=dep[x]+(sy-sx+now)%now;
a.y=dep[y];
b.x=dep[x];
b.y=dep[y]+(sx-sy+now)%now;
pe ans=cmp(a,b);
printf("%d %d\n",ans.x,ans.y);
}
}
return 0;
}
遇题先分析哪种基环树(森林),第二步找环,第三步处理需要的信息,第四步就是按照题目要求统计答案,注意我们基环树对sum的定义方式
对于基环树上的DP,无非就是树形DP还要处理环的叠加,众所周知,我们对于带环的DP处理方式有两种,一是进行两次DP,一次断开,一次强制连接(通过赋值特判等实现),另一个是将环复制一倍进行DP
对于基环树上的DP,这两种做法也有着不同的实现方式
第一种做法与树形DP相似,只是加入了特判
第二种做法需要我们将子树信息统计完整之后直接转为序列DP
当然应用得更多的还是第一种
下面以一道例题来说明这种情况
上帝手中有 N 种世界元素,每种元素可以限制另外 1 种元素,把第 i 种世界元素能够限制的那种世界元素记为 a[i]。
现在,上帝要把它们中的一部分投放到一个新的空间中去建造世界。
为了世界的和平与安宁,上帝希望所有被投放的世界元素都有至少一个没有被投放的世界元素限制它。
上帝希望知道,在此前提下,他最多可以投放多少种世界元素?
因为每一种元素可以限制另外一种元素,于是如果我们将 i , a [ i ] i,a[i] i,a[i]进行连边的话,就会形成一个基环树森林,但是我们仍然需要注意的是,我们该建立内向基环树森林还是外向基环树森林
于是我们来思考哪一种方式更加容易实现代码
这道题很明显是一道DP题,如果我们设 f [ u , 0 / 1 ] f[u,0/1] f[u,0/1]表示(不)放节点u的时候,最多可以投放多少元素,容易写出状态转移方程
f [ u , 0 ] = ∑ a [ v ] = u max ( f [ v , 0 ] , f [ v , 1 ] ) f[u,0]=\sum_{a[v]=u}\max(f[v,0],f[v,1]) f[u,0]=a[v]=u∑max(f[v,0],f[v,1])
上式的含义是:当不放置节点 u u u的时候,所有限制它的元素都可以放置,类似的,若放置 u u u,则方程为
f [ u , 1 ] = 1 + max a [ x ] = u { f [ x , 0 ] + ∑ a [ v ] = u , v ≠ x max ( f [ v , 0 ] , f [ v , 1 ] ) } f[u,1]=1+\max_{a[x]=u}\lbrace f[x,0]+\sum_{a[v]=u,v\ne x}\max(f[v,0],f[v,1])\rbrace f[u,1]=1+a[x]=umax{f[x,0]+a[v]=u,v=x∑max(f[v,0],f[v,1])}
上式的含义是,如果要投放元素 u u u,则至少有一个限制它的元素不投放,剩余的随意
对于这个状态转移方程式,它告诉我们,我们需要快速使用 x x x查找到所有 a [ v ] = x a[v]=x a[v]=x的 v v v,于是我们不妨把 x x x的子节点都设为 a [ v ] = x a[v]=x a[v]=x的节点 v v v,具体的,我们把每个 a [ i ] a[i] a[i]向 i i i连有向边, a [ i ] a[i] a[i]为父节点
于是乎,我们就建立了一个每个节点有且仅有一条入边的有向图,即外向基环树森林
所以我们可以使用不断跳父亲节点的方式找到环,跳到最后的两个节点 p , a [ p ] p,a[p] p,a[p],我们断开它们的连接,在以p为根节点的树上进行一次DP,这一次DP就相当于没有用上 p p p可以限制 a [ p ] a[p] a[p]的条件,两个答案都可以更新
第一次DP只有一种情况没有考虑到,即 a [ p ] a[p] a[p]被 p p p所限制,对于这个的解决办法就是,我们进行第二次DP,通过特判使得其强制性的被 p p p所限制,即dp到节点 a [ p ] a[p] a[p]的时候,强制性的令 f [ a [ p ] , 1 ] = f [ a [ p ] , 0 ] + 1 f[a[p],1]=f[a[p],0]+1 f[a[p],1]=f[a[p],0]+1,也即不需要任何一个子节点来限制它(已被p所限制),最后的答案我们就只取 f [ p , 0 ] f[p,0] f[p,0]更新答案即可
#define N 1000005
int n,a[N],root,f[N][2],ans,head[N],ver[N],nxt[N],tot,vis[N];
void add(int u,int v){
nxt[++tot]=head[u],ver[tot]=v,head[u]=tot;
}
int dp(int u,int t){
f[u][0]=f[u][1]=0;
vis[u]=1;
int mn=0x3f3f3f3f;
for(int i=head[u];i;i=nxt[i]){
int v=ver[i];
if(v!=root){
int x=dp(v,t);
mn=min(mn,x-f[v][0]),f[u][0]+=x;
}
}
f[u][1]=f[u][0]-mn+1;
if(t&&u==a[root])f[u][1]+=mn;
return max(f[u][0],f[u][1]);
}
int solve(int u){
root=u;
while(!vis[a[root]])vis[root]=1,root=a[root];
int ans=dp(root,0);
dp(root,1);
return max(ans,f[root][0]);
}
int main() {
scanf("%d",&n);
for(int i=1;i<=n;i++){
scanf("%d",&a[i]);
add(a[i],i);
}
for(int i=1;i<=n;i++)if(!vis[i])ans+=solve(i);
printf("%d\n",ans);
return 0;
}
本节知识点及需要背的板子:
简单点说,就是我们的图上存在着一个环,使得环上总边权为负,这样的的环被称为负环,类似的,我们也有对正环的定义,需要注意的是,无向图中我们按两条相反有向边储存本身就等于是一个自环
对于存在负环的图,最短路问题永远不可能求出解,因为负环的存在会导致环上节点的三角不等式永远无法收敛,因为跑圈的同时会无限更新
类似的,对于存在正环的图,最长路问题也永远不可能求出解
介于正环和负环之间的就是零环,在一些题目中,零环没有任何意义,往往需要缩点缩掉
至于负环的求法,根据抽屉原理,一个负环必定会存在负权边,而存在负权边的最短路问题一般是采用 B e l l m a n − f o r d Bellman-ford Bellman−ford或者是 S P F A SPFA SPFA算法处理,类似的,我们可以使用它们来判断负环
边权为负的无向边本身就是一个负环,很多时候有向图数据经过构造有可能会出现重边,反向边等,需要注意,必要时特判
bool spfa(int mid){
memset(dis,0x3f,sizeof dis);
memset(cnt,0,sizeof cnt);
queueq;
dis[0]=0;
q.push(0);
while(!q.empty()){
int u=q.front();q.pop();
for(int i=head[u];i;i=nxt[i]){
int v=ver[i],w=cost[i];
if(dis[v]>dis[u]+w){
dis[v]=dis[u]+w;
cnt[v]=cnt[u]+1;
if(cnt[v]>=n)return 0;//存在负环
q.push(v);
}
}
}
return 1;//不存在负环
}
同理,正环的求法就是把最短路 S P F A SPFA SPFA换成最长路 S P F A SPFA SPFA,照样更新即可
幻影国建成了当今世界上最先进的高铁,该国高铁分为以下几类:
S S S—高速光子动力列车—时速 1000 k m / h 1000km/h 1000km/h
G G G—高速动车—时速 500 k m / h 500km/h 500km/h
D D D—动车组—时速 300 k m / h 300km/h 300km/h
T T T—特快—时速 200 k m / h 200km/h 200km/h
K K K—快速—时速 150 k m / h 150km/h 150km/h
该国列车车次标号由上述字母开头,后面跟着一个正整数 ( ≤ 1000 ) (≤1000) (≤1000)构成。
由于该国地形起伏不平,各地铁路的适宜运行速度不同。
因此该国的每一条行车路线都由 K K K 列车次构成。
例如: K = 5 K=5 K=5 的一条路线为: T 120 − D 135 − S 1 − G 12 − K 856 T120−D135−S1−G12−K856 T120−D135−S1−G12−K856。
当某一条路线的末尾车次与另一条路线的开头车次相同时,这两条路线可以连接起来变为一条更长的行车路线。
显然若干条路线连接起来有可能构成一个环。
若有 3 条行车路线分别为:
x 1 − x 2 − x 3 x1−x2−x3 x1−x2−x3
x 3 − x 4 x3−x4 x3−x4
x 4 − x 5 − x 1 x4−x5−x1 x4−x5−x1
x 1 ∼ x 5 x1∼x5 x1∼x5 车次的速度分别为 v 1 ∼ v 5 v1∼v5 v1∼v5。
定义高铁环的值为(环上各条行车路线速度和)的平均值,即:
[ ( v 1 + v 2 + v 3 ) + ( v 3 + v 4 ) + ( v 4 + v 5 + v 1 ) ] / 3 [(v1+v2+v3)+(v3+v4)+(v4+v5+v1)]/3 [(v1+v2+v3)+(v3+v4)+(v4+v5+v1)]/3
所有高铁环的值的最大值称为最优高铁环的值。
给出 M M M 条行车路线,求最优高铁环的值(四舍五入为整数)。
首先这个路线内部不需要管是什么,只需要知道两端即可,一条路线就等于是一条边,边权就是速度
不难发现,这道题涉及除法,并且最优高铁环的值明显可以二分,这就是一个0/1分数规划问题
我们设一个环为 G = ( V , E ) G=(V,E) G=(V,E), V V V是点集, E E E是边集,那么我们的答案就是找到一个 G G G,使得下式值最大
∑ i ∈ E c o s t [ i ] ∑ u ∈ V 1 = ∑ i ∈ E c o s t [ i ] ∣ V ∣ \frac{\sum_{i\in E}cost[i]}{\sum_{u\in V}1}=\frac{\sum_{i\in E}cost[i]}{|V|} ∑u∈V1∑i∈Ecost[i]=∣V∣∑i∈Ecost[i]
直接寻找明显很困难,下式又可以二分答案,那么我们考虑使用二分答案,设二分的值为 m i d mid mid,经过变式,有两种可能性
1.
∑ i ∈ E c o s t [ i ] ∑ u ∈ V 1 ≤ m i d \frac{\sum_{i\in E}cost[i]}{\sum_{u\in V}1}\le mid ∑u∈V1∑i∈Ecost[i]≤mid
2.
∑ i ∈ E c o s t [ i ] ∑ u ∈ V 1 > m i d \frac{\sum_{i\in E}cost[i]}{\sum_{u\in V}1}>mid ∑u∈V1∑i∈Ecost[i]>mid
我们以2为例子进行分析
因为是有向图,所以环上的边一样可以使用以这条边为入边的节点来作为长度
变式即得
∑ u ∈ V ( m i d − c o s t [ u ] ) < 0 \sum_{u\in V}(mid-cost[u])<0 u∈V∑(mid−cost[u])<0
此时这个环就好找了,就是判定图中有没有负环,若有负环说明此式成立,令 l = m i d l=mid l=mid,否则令 r = m i d r=mid r=mid,二分结束时,就得到了答案
在本题中存在特殊构造的数据,具有重边和自环,对于每一个自环,答案一定不会小于这些自环的边权,在代码中见最后的一个 max \max max,至于重边,使用贪心不难证明重边只需要保留边权最小的一条即可
另外,本题数据非常紧,达到了 50000 50000 50000的程度,需要使用上文所说的优化1进行优化,经实际测试50倍足以通过
#define N 50050
int num,ver[N],nxt[N],head[N],tot;float cost[N];float dis[N];int cnt[N],n,m,ms,to,mn=2e9;
struct node{
int u,v,w;
}que[N];
mapH;
map,pair >edge;//判断重边,自环
int get(string n){
if(!H[n])H[n]=++num;
return H[n];
}//离散化,字符串化整数
int get_cost(char n){
if(n=='S')return 1000;
if(n=='G')return 500;
if(n=='D')return 300;
if(n=='T')return 200;
if(n=='K')return 150;
return 0;
}//得到权值
void add(int u,int v,int w){
nxt[++tot]=head[u],ver[tot]=v,cost[tot]=w,head[u]=tot;
}
void in(string x){
int u=-1,v=-1,w=0,len=x.size();
string y="";
for(int i=0;iq;
q.push(1);
dis[1]=0;
int t=0;
while(!q.empty()){
t++;
if(m>40000&&t>400000)return false;//优化1
int u=q.front();q.pop();
for(int i=head[u];i;i=nxt[i]){
int v=ver[i];
if(dis[v]>dis[u]+cost[i]){
cnt[v]=cnt[u]+1;
if(cnt[v]>=num)return false;
q.push(v);
dis[v]=dis[u]+cost[i];
}
}
}
return true;
}
float solve(){
float l=0,r=ms;
while(r-l>1e-1){
float mid=(l+r)/2;
init(mid);
if(spfa())r=mid;
else l=mid;
}
return l<1e-1?-1.5:l;//四舍五入
}
int main(){
cin>>m;
for(int i=1;i<=m;i++){
string x;
cin>>x;
in(x);
}
//puts("AS");
int ans=solve()+0.5;
if(mn<2e9)ans=max(ans,mn);
printf("%d",ans);
}
所谓差分约束系统是指一个包含 X 1 ∼ X n X_1\sim X_n X1∼Xn的未知数, m m m个限制条件,每个限制条件是形如 X i − X j ≤ c k X_i-X_j\le c_k Xi−Xj≤ck的不等式,其中 c k c_k ck是任意常数,求其一组合法解
很明显,若我们找到了一组合法解,设为 a 1 ∼ a n a_1\sim a_n a1∼an,那么 a + Δ , a 2 + Δ … … a n + Δ a+\Delta,a_2+\Delta……a_n+\Delta a+Δ,a2+Δ……an+Δ也是一组合法解,其中 Δ \Delta Δ为任意实数,因为两个变量做差会消去 Δ \Delta Δ
所以我们完全可以限制先找到一组负数解,然后通过变换找出所有解
我们发现, X i − X j ≤ c k X_i-X_j\le c_k Xi−Xj≤ck进行变式之后 X i ≤ X j + c k X_i\le X_j+c_k Xi≤Xj+ck,这与三角形不等式很相似,这启发我们使用 S P F A SPFA SPFA将其转换为图论问题进行求解,具体的,我们对于每一个约束条件都在图上加入边 ( j , i , c k ) (j,i,c_k) (j,i,ck),注意是 j − > i j->i j−>i的有向边,最后如果这张图跑 S P F A SPFA SPFA最短路最后能够收敛(无负环),就说明这个差分约束系统有解,其中一组解为 d i s t dist dist数组,若无法收敛(存在负环),则差分约束系统无解
在实际应用中,差分约束系统常常不会将所有的限制条件摆在明面上,我们还需要在题目上挖掘隐藏条件使得解有意义,例如一个非严格单调递增序列就具备隐含条件, s k ≥ s k − 1 s_k\ge s_{k-1} sk≥sk−1,这些往往可以从答案的相对大小关系,答案有意义的条件等方面进行寻找。有时,这个差分约束系统会变样,比如变为 X i − X j ≥ c k X_i-X_j\ge c_k Xi−Xj≥ck,遇到这种情况一个解决办法是照样建图,找正环,最长路,另一个方案就是变式为