《算法竞赛进阶指南》------图论篇2

0x0E 雨天的尾巴 洛谷p4556(线段树合并+树上差分+树链lca)

雨天的尾巴
详细视频讲解
题意:一个由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 − 1 n-1 n1条边,接下来m行,每行 x , y , z , x,y,z, xyz代表一次救济粮的发放是从 x x x y y y 路径上的每栋房子发放了一袋 z z z 类型的救济粮。。
输出:输出 n n n 行,每行一个整数,第 i i i 行的整数代表 ii 号房屋存放最多的救济粮的种类,如果有多种救济粮都是存放最多的,输出种类编号最小的一种。
如果某座房屋没有救济粮,则输出 0 0 0
思路:使用树链剖分求lca。使用树上差分,在路径两边,公共祖先,及其公共祖先父节点添加标记。
此题求点上存放最多救济粮的编号,建立线段树的,区间表示种类编号。
修改过程,配合树上差分即是单点修改。合并时,儿子结点向上更新取最大的儿子结点给父节点。
ACcode(详解)

#include<bits/stdc++.h>
#define ll long long
#define ld long double
#define ull unsigned long long
#define rep(i,a,b) for(int i=a;i<=b;i++)
ll gcd(ll a,ll b){ return b? gcd(b,a%b):a;}
const int maxn=1e5+10;
const ll P=1e9+7;
ll read(){
    ll s = 0, f = 1; char ch = getchar();
    while(!isdigit(ch)){
        if(ch == '-') f = -1;
        ch = getchar();
    }
    while(isdigit(ch)) s = (s << 3) + (s << 1) + (ch ^ 48), ch = getchar();
    return s * f;
}
using namespace std;
typedef pair<int,int> pii;
struct E{
    int v,next;
}Edge[maxn<<1];
int tot,head[maxn<<1];
void AddEdge(int u,int v){
    Edge[++tot] = (E){v,head[u]};
    head[u] = tot;
}
struct Node{
    int l,r;  //左右结点编号
    pii val;
}sgt[70*maxn];
int cnt;
/// deep深度    siz当前结点的子结点之和  fa:父节点  son:son[u] =  儿子结点子树最多的结点。 //树链时用到
int deep[maxn],siz[maxn],fa[maxn],son[maxn];  
int root[maxn];    //存放结点的根编号
// dfs1  和 dfs2 求树链
void dfs1(int u,int f){
    deep[u] = deep[f] + 1;
    siz[u]  = 1;
    fa[u] = f;
    int maxsize = -1;
    for(int i=head[u];i;i=Edge[i].next){
        int v= Edge[i].v;
        if(v==f) continue;
        dfs1(v,u);
        siz[u] +=siz[v];
        if(siz[v] > maxsize){
            maxsize = siz[v];
            son[u] = v;
        }
    }
}
int top[maxn];     // 一条链的所有点标记一样
void dfs2(int u,int k){
    top[u] = k;
    if(son[u]) dfs2(son[u],k);
    for(int i=head[u];i;i=Edge[i].next){
        int v = Edge[i].v;
        if(v==fa[u] || v==son[u]) continue;
        dfs2(v,v);
    }
}
// 求lca
int lca(int x,int y){
    while(top[x]!= top[y]){
        if(deep[top[x]] < deep[top[y]]){
            swap(x,y);
        }
        x = fa[top[x]];
    }
    return deep[x]<deep[y]? x:y;
}
// 比较儿子结点val大小
pii& maxpii(pii& x,pii& y){  // 个数  和 类别
        if(x.first < y.first) return y;
        else if(x.first == y.first) return x.second<y.second? x:y;
        else return x;
}
// 向上更新
void pushup(int k){ sgt[k].val = maxpii(sgt[sgt[k].l].val , sgt[sgt[k].r].val); }
//单点修改 区间[l,r]  k--- root[u]的地址映射值,对k修改同时对root[u]修改,  p种类 x值
void modify(int l,int r,int& k,int p,int x){
    if(!k)  k = ++cnt;   // 结点null 创建一个空间
    if(l==r){   //叶子结点 ++
        sgt[k].val.first +=x;
        sgt[k].val.second = p;
        return ;
    }
    int  m = (l+r)>>1;
    //  线段树常见手法
    if(p<=m){
        modify(l,m,sgt[k].l,p,x);
    }else{
        modify(m+1,r,sgt[k].r,p,x);
    }
    // 向上更新
    pushup(k);
}
// y合并到x中. 1.结点空 2.非空合并 3.叶子结点
void merge(int &x,int y,int l=1,int r=maxn){
    if((!x) || (!y)) x|=y;
    else if(l==r){
        sgt[x].val.first +=sgt[y].val.first;

    }else{
        int m = (l+r)>>1;
        merge(sgt[x].l,sgt[y].l,l,m);
        merge(sgt[x].r,sgt[y].r,m+1,r);
        pushup(x);
    }
}
int ans[maxn];  // 存结果
// 对树自底向上回溯,合并。
void dfs(int u){
    for(int i=head[u];i;i=Edge[i].next){
        int v= Edge[i].v;
        if(v==fa[u]) continue;
        dfs(v);
        merge(root[u],root[v]);
    }
    if(sgt[root[u]].val.first){
        ans[u] = sgt[root[u]].val.second;
    }
}
void solve(){
    int n,m;
    cin>>n>>m;
    for(int i=1;i<n;i++){
        int u,v;
        cin>>u>>v;
        AddEdge(u,v);
        AddEdge(v,u);
    }
    dfs1(1,0);
    dfs2(1,1);
    // cout<<"I am Hero"<<endl;
    // for(int i=1;i<=n;i++){
    //     cout<<fa[i]<<" "<<i<<endl;
    // }
    for(int i=1;i<=m;i++){
        int x,y,z;
        cin>>x>>y>>z;
        // cout<<lca(x,y)<<endl;
        // 树上差分常见手法
        modify(1,maxn,root[x],z,1); // x结点加上z救济粮标记
        modify(1,maxn,root[y],z,1);// y结点加上z救济粮标记
        modify(1,maxn,root[lca(x,y)],z,-1);  // lca(x,y)结点加上-z救济粮标记
        modify(1,maxn,root[fa[lca(x,y)]],z,-1);// fa[lca(x,y)]结点加上-z救济粮标记
    } 
    dfs(1);   //差分统计咯
    for(int i=1;i<=n;i++){
        cout<<ans[i]<<endl;
    }
    return ;
}
int main (){
  //freopen("in.txt","r",stdin);
  //freopen("out.txt","w",stdout);
  solve();
  getchar();
  getchar();
  return 0;
}

0x0F CF600E Lomsat gelral(线段树合并)

CF600E Lomsat gelral
题意:给你一棵有nn个点的树 ( n ≤ 1 0 5 ) (n≤10^5) (n105),树上每个节点都有一种颜色 c i ( c i ≤ n ) ci(ci≤n) ci(cin),让你求每个点子树出现最多的颜色的编号的和.
思路:一样的只不过单点修改对每个点修改。线段树的区间依然表示的是颜色编号。
在pushup上 个数相等的儿子结点,编号相加。不等,取个数较大的赋给父节点。
tips: 记得开long long 。 (话说假如树退化成一条链,不会爆栈嘛,好像也让过了…)
ACcode:

#include<bits/stdc++.h>
#define ll long long
#define ld long double
#define ull unsigned long long
#define rep(i,a,b) for(int i=a;i<=b;i++)
ll gcd(ll a,ll b){ return b? gcd(b,a%b):a;}
const int N=1e5+10;
const ll P=1e9+7;
ll read(){
    ll s = 0, f = 1; char ch = getchar();
    while(!isdigit(ch)){
        if(ch == '-') f = -1;
        ch = getchar();
    }
    while(isdigit(ch)) s = (s << 3) + (s << 1) + (ch ^ 48), ch = getchar();
    return s * f;
}
using namespace std;
typedef pair<ll,ll> pii;
struct Node{
    ll l,r;
    pii val;  // 个数 类型
}sgt[30*N];
ll cnt;
ll root[N];
struct E{ ll v,next;} Edge[N<<1];
ll tot,head[N];
void AddEdge(ll u,ll v){
    Edge[++tot] = (E){v,head[u]};
    head[u] = tot;
}
void pushup(ll k){
    ll l,r;
    l = sgt[k].l;
    r = sgt[k].r;
    if(sgt[l].val.first == sgt[r].val.first){
        sgt[k].val.first = sgt[l].val.first;
        sgt[k].val.second = sgt[l].val.second + sgt[r].val.second;
    }else if( sgt[l].val.first < sgt[r].val.first){
        sgt[k].val.first  =sgt[r].val.first;
        sgt[k].val.second = sgt[r].val.second;
    }else {
        sgt[k].val.first  =sgt[l].val.first;
        sgt[k].val.second = sgt[l].val.second;
    }
}
void modify(ll l,ll r,ll& k,ll p,ll x){
    if(!k) k = ++cnt;
    if(l==r){
        sgt[k].val.first+=x;
        sgt[k].val.second = p;
        return ;
    }
    ll m = (l+r)>>1;
    if(p<=m) modify(l,m,sgt[k].l,p,x);
    else modify(m+1,r,sgt[k].r,p,x);
    pushup(k);    
}
void merge(ll& x,ll y,ll l=1,ll r=N){
    if((!x)|| (!y))  x|=y;
    else if(l==r){
        sgt[x].val.first +=sgt[y].val.first;
    }else{
        int m= (l+r)>>1;
        merge(sgt[x].l,sgt[y].l,l,m);
        merge(sgt[x].r,sgt[y].r,m+1,r);
        pushup(x);
    }
}
ll ans[N];  //存放结果
//  自底向上的递归合并
void dfs(int u,int f){
    for(int i=head[u];i;i=Edge[i].next){
        int v = Edge[i].v;
        if(v==f) continue;
        dfs(v,u);
        merge(root[u],root[v]);
    }
    ans[u] = sgt[root[u]].val.second;
}
void solve(){
    int n;
    cin>>n;
    for(int i=1;i<=n;i++){
        int p;
        cin>>p;
        modify(1,N,root[i],p,1);
    }
    //建树
    for(int i=1;i<n;i++){
        int u,v;
        cin>>u>>v;
        AddEdge(u,v);
        AddEdge(v,u);
    }
    dfs(1,0);
    for(int i=1;i<=n;i++){
        cout<<ans[i]<<" ";
    }
}
int main (){
//   freopen("in.txt","r",stdin);
//   freopen("out.txt","w",stdout);
  solve();
  getchar();
  getchar();
  return 0;
}

0x10 天天爱跑步 NOIP2016 P1600 (树链LCA 和树上差分)

天天爱跑步
详解
题意:再一个树形结构的地图中,m(m<=3e5)个玩家,起点 S i S_i Si,终点 T i T_i Ti,所有玩家从第0秒同时出发。由于地图是树形结构,玩家从起点到终点路径唯一。在地图上每个结点都有观察员,在j结点的观察员选择在第 W j W_j Wj秒观察玩家。一个玩家能被该观察员观察到当且仅当在第 W j W_j Wj也正好到达结点j。求每个结点的观察员可以观察的玩家数量。
思路 : 对问题拆解,玩家起点s[i],终点t[i],起点和终点公共祖先lca.观察员j点第w[j]秒。dist:起点到终点的距离
玩家被观察员j观察到的条件:起点到lca:deep[j]+w[j] = deep[s[i]] 。 或者 lca到终点 dist - deep[t[i]] = w[j] - deep[j];
在式子中,我们就能将deep[s[i]], dist - deep[t[i]] 看成一个类型,添加上两个公共数组中。每当询问j点时,就在公共数组中询问类型为deep[j]+w[j],w[j] - deep[j]的个数。 结果就能得出了。
注意!求一个j点观察到的数量,在dfs遍历,有效的数量是子树的范围呢。在没到j点之前的值是无效的。有效是"遍历完成时的值"与“”遍历开始时的值”。由于我们是将一条路径拆成两部分:起点到lca,lca到终点。细心的你一定发现了lca被用了两次,假如lca有贡献,那么势必会贡献两次,所以我们在此先将结果值减一,if(deep[LCA]+w[LCA]==deep[s[i]]) ans[LCA]–;
w[j] - deep[j] 这个类型值可能为负数,所以在原有加上N,即是类型值是(w[j] - deep[j]+N);
ACcode详解

#include<bits/stdc++.h>
#define ll long long
#define ld long double
#define ull unsigned long long
#define rep(i,a,b) for(int i=a;i<=b;i++)
ll gcd(ll a,ll b){ return b? gcd(b,a%b):a;}
const int N=3e5+10;
const ll P=1e9+7;
ll read(){
    ll s = 0, f = 1; char ch = getchar();
    while(!isdigit(ch)){
        if(ch == '-') f = -1;
        ch = getchar();
    }
    while(isdigit(ch)) s = (s << 3) + (s << 1) + (ch ^ 48), ch = getchar();
    return s * f;
}
using namespace std;
struct E{ int to,next;}Edge[N<<1];
int tot,head[N];
void AddEdge(int u,int v){
     Edge[++tot] = (E){v,head[u]};
     head[u] = tot;
}
int deep[N],fa[N],siz[N],son[N];  // 深度,fa[子]=父,siz[u]=u子树结点数量+1 son[u]= u的子结点中siz最大的节点
// dfs1,dfs2 使用树链的方式求LCA
void dfs1(int u,int f){
    fa[u] = f; deep[u] = deep[f]+1; siz[u] =1;
    int maxsize = -1;
    for(int i=head[u];i;i=Edge[i].next){
        int v = Edge[i].to;
        if(v==f) continue;
        dfs1(v,u);
        siz[u] +=siz[v];
        if(siz[v] > maxsize){
            maxsize  = siz[v];
            son[u] = v;
        }
    }
}
int top[N];   
void dfs2(int u,int k){
    top[u] = k;
    if(son[u]) dfs2(son[u],k);
    for(int i=head[u];i;i=Edge[i].next){
        int v = Edge[i].to;
        if(v==fa[u] || v==son[u]) continue;
        dfs2(v,v);
    }
}
int lca(int x,int y){
    while(top[x]!=top[y]){
        if(deep[top[x]] < deep[top[y]]) swap(x,y);
        x = fa[top[x]];
    }
    return deep[x]<deep[y]? x:y;
}
int w[N],s[N],t[N];  
vector<int> e1[N],e2[N];  // 终点 编号   lca 编号 
int js[N];  // 起点  个数
int ans[N];  //存放结果
int b1[N],b2[N<<1]; // 起点开始  lca开始
int dist[N];  // 求出路径的距离
// dfs递归自叶向根
void dfs(int x){
    int t1 = b1[deep[x] + w[x]];
    int t2 = b2[w[x] - deep[x] + N];
    for(int i=head[x];i;i=Edge[i].next){
        int v = Edge[i].to;
        if(v==fa[x])  continue;
        dfs(v);
    }
    b1[deep[x]] +=js[x];
    for(int i=0;i<(int)e1[x].size();i++){
        int num = e1[x][i];  //第几条边
        b2[dist[num] - deep[t[num]] + N ]++;
    }
    // 计算结果
    ans[x] += b1[deep[x] + w[x] ] - t1 + b2[w[x] -deep[x]+N] -t2;
    // x是LCA时   路径已经走完。dfs回溯是向上
    for(int i = 0;i<(int)e2[x].size();i++){
        int num = e2[x][i];
        b1[deep[s[num]]] -- ;
        b2[dist[num] - deep[t[num]]+ N ]--;
    }
}
void solve(){
    int n,m;
    cin>>n>>m;
    for(int i=1;i<n;i++){
        int u,v;
        cin>>u>>v;
        AddEdge(u,v);
        AddEdge(v,u);
    }
    for(int i=1;i<=n;i++) cin>>w[i];
    dfs1(1,0);
    dfs2(1,1);
    for(int i=1;i<=m;i++){
        cin>>s[i]>>t[i];
        js[s[i]]++;  
        int LCA = lca(s[i],t[i]);
        dist[i] =  deep[s[i]]+ deep[t[i]] - 2*deep[LCA];
        // cout<<"LCA:"<<LCA<<endl;
        e1[t[i]].push_back(i);
        e2[LCA].push_back(i);
        if(deep[LCA]+w[LCA]==deep[s[i]]) ans[LCA]--;
    }
    dfs(1);
    for(int i=1;i<=n;i++){
        cout<<ans[i]<<" ";
    }
}
int main (){
  //freopen("in.txt","r",stdin);
  //freopen("out.txt","w",stdout);
  solve();
  getchar();
  getchar();
  return 0;
}

0x11 异象石 Acwing (树链LCA + 时间戳)

异象石

题意:在一颗树种,三种操作:+,-和?.
在这里插入图片描述
输出查询的结果。
所以的点连通的边集的总长度最小什么意思呢? 即是这些点形成的连通图的权值最小值。
使用时间戳,对时间相邻的点求得路径长度.
ACcode:

#include<bits/stdc++.h>
#define ll long long
#define ld long double
#define ull unsigned long long
#define rep(i,a,b) for(int i=a;i<=b;i++)
ll gcd(ll a,ll b){ return b? gcd(b,a%b):a;}
const int N=2e5+10;
const ll P=1e9+7;
ll read(){
    ll s = 0, f = 1; char ch = getchar();
    while(!isdigit(ch)){
        if(ch == '-') f = -1;
        ch = getchar();
    }
    while(isdigit(ch)) s = (s << 3) + (s << 1) + (ch ^ 48), ch = getchar();
    return s * f;
}
using namespace std;
struct E{ ll to,next,val;} Edge[N<<1];
int tot=0,head[N];
void AddEdge(int u,int v,ll val){
    Edge[++tot] = (E){v,head[u],val};
    head[u] = tot;
}
ll dist[N],fa[N],siz[N],son[N],deep[N];
ll dfn[N],mp[N];
ll cnt=0;
void dfs1(int u,int f){
    fa[u] = f;
    siz[u] = 1;
    dfn[u] = ++cnt;  
    mp[cnt] = u;  // 编号映射到节点
    deep[u] = deep[f] +1;
    int maxsize = -1;
    for(int i=head[u];i;i=Edge[i].next){
        int v = Edge[i].to;
        ll val = Edge[i].val;
        if(v==f) continue;
        dist[v] =  dist[u] + val;
        dfs1(v,u);
        siz[u] +=siz[v];
        if(siz[v] > maxsize){
            son[u] = v;
            maxsize = siz[v];
        }
    }
}
int top[N];
void dfs2(int u,int k){
    top[u] = k;
    if(son[u]) dfs2(son[u],k);
    for(int i=head[u];i;i=Edge[i].next){
        int v = Edge[i].to;
        if(v==fa[u] || v==son[u]) continue;
        dfs2(v,v);
    }
}
int lca(int x,int y){
    while(top[x]!=top[y]){
        if(deep[top[x]] < deep[top[y]]) swap(x,y);
        x = fa[top[x]];
    }
    return deep[x]<deep[y]? x:y;
}
ll dis(int u,int v){ //编号
    u = mp[u];
    v = mp[v];
   // printf("u:%d v:%d lca:%d\n",u,v,lca(u,v));
    return dist[u] + dist[v] - 2*dist[lca(u,v)];
}
set<ll> s; //存放编号 
ll ans = 0;
void add(int u){
    s.insert(dfn[u]);
    set<ll>:: iterator it = s.find(dfn[u]);
    set<ll>:: iterator l = it==s.begin() ? --s.end():--it;
    it = s.find(dfn[u]);
    set<ll>:: iterator r = it== (--s.end()) ? s.begin():++it;
    it = s.find(dfn[u]);
    ans -= dis(*l,*r);
    ans +=dis(*l,*it) + dis(*r,*it);
}
void del(int u){
   set<ll>:: iterator it = s.find(dfn[u]);
   set<ll>:: iterator l = it==s.begin() ? --s.end():--it;
   it = s.find(dfn[u]);
   set<ll>:: iterator r = it== (--s.end()) ? s.begin():++it;
   it = s.find(dfn[u]);
   ans += dis(*l,*r);
   ans -= dis(*l,*it) + dis(*it,*r);
   s.erase(dfn[u]); 
}
void solve(){
    int n;
    cin>>n;
    for(int i=1;i<n;i++){
        int u,v;
        ll val;
        cin>>u>>v>>val;
        AddEdge(u,v,val);
        AddEdge(v,u,val);
    }
    dfs1(1,0);
    dfs2(1,1);
    char ch;
    int m;
    cin>>m;
    int u;
    for(int i=1;i<=m;i++){
        cin>>ch;
        if(ch=='+'){
            cin>>u;
            add(u);
        }else if(ch=='-'){
            cin>>u;
            del(u);
        }else if(ch=='?'){
            cout<<ans/2<<endl;
        }
    }
}
int main (){
  //freopen("in.txt","r",stdin);
  //freopen("out.txt","w",stdout);
  solve();
  getchar();
  getchar();
  return 0;
}

0x12 次小生成树 (倍增LCA + 路径上权值最大和次大的保存)

次小生成树
题意:给定一张 N 个点 M 条边的无向图,求无向图的严格次小生成树。设最小生成树的边权之和为 sum,严格次小生成树就是指边权之和大于 sum 的生成树中最小的一个。 问次大生成树的边集和是多少。
思路: 不用质疑,先kursal找出最小生成树,并建立树。这里要明白一点 未在最小生成树的边,权值一定大于等于树上x->y路径边的权值.
就开始替换咯,找出树x-y路径的权值最大(记为max1),次大记为(记为max2)。如果外边权值==max1,那么 s u m − m a x 2 + v a l sum-max2+val summax2+val。否则 s u m − m a x 2 + v a l sum-max2+val summax2+val.
在这里插入图片描述
在这里插入图片描述
G[y][0][1] = -INF; //重边或重边权值都一样 很重要的一条代码

// 在四个值中 筛选出最大给max1。严格次大给max2 巧妙构思
void qu(ll &max1,ll &max2,ll x,ll y){
    if(max1 == x){
        max2 = max(max2,y);
    }else if(max1 < x){
        max2 = max(max1,y);
        max1  = x;
    }else if(max1 > x){
        max2 = max(max2,x);
    }
}

ACcode详解

#include<bits/stdc++.h>
#define ll long long
#define ld long double
#define ull unsigned long long
#define rep(i,a,b) for(int i=a;i<=b;i++)
ll gcd(ll a,ll b){ return b? gcd(b,a%b):a;}
const int N=2e5+10;
const ll P=1e9+7;
const ll INF = 1e15;
ll read(){
    ll s = 0, f = 1; char ch = getchar();
    while(!isdigit(ch)){
        if(ch == '-') f = -1;
        ch = getchar();
    }
    while(isdigit(ch)) s = (s << 3) + (s << 1) + (ch ^ 48), ch = getchar();
    return s * f;
}
using namespace std;
struct Node{ ll u,v,vis,val;} a[N<<1];
struct E{ ll to,next,val;}  Edge[N];
int tot,head[N];
ll sum,ans;
void AddEdge(int u,int v,ll val){
    Edge[++tot] = (E) {v,head[u],val};
    head[u] = tot;
}
ll fa[N];
int find(int x){
    return x==fa[x]? x:fa[x] = find(fa[x]);
}
bool cmp(Node x,Node y){
    return x.val < y.val;
}
void krusal(int n,int m){
    // 权值排序
    sort(a+1,a+1+m,cmp);
    for(int i=1;i<=m;i++){
        int x = find(a[i].u);
        int y = find(a[i].v);
        if(x==y) continue;
        a[i].vis = 1;
        fa[x] = y;
        sum +=a[i].val;
        AddEdge(a[i].u,a[i].v,a[i].val);
        AddEdge(a[i].v,a[i].u,a[i].val);
    }
}
ll f[N][20],deep[N];
int t ;
ll G[N][20][2];  // 节点 k 大小
// bfs 求 fa和G
void bfs(){
    queue<int> q;
    q.push(1);
    deep[1] = 1;
    while(q.size()){
        int x = q.front();
        q.pop();
        for(int i=head[x];i;i=Edge[i].next){
            int y = Edge[i].to;
            if(deep[y]) continue;
            deep[y] = deep[x] + 1;
            f[y][0] = x;
            G[y][0][0] = Edge[i].val;
            G[y][0][1] = -INF;  //重边或重边权值都一样
            for(int i=1;i<=t;i++){
                f[y][i] = f[f[y][i-1]][i-1];
                int z = f[y][i-1];
                G[y][i][0] = max(G[y][i-1][0],G[z][i-1][0]);
                if(G[y][i-1][0] == G[z][i-1][0]){
                    G[y][i][1] = max(G[y][i-1][1],G[z][i-1][1]);
                }else if(G[y][i-1][0] < G[z][i-1][0]){
                    G[y][i][1] = max(G[y][i-1][0],G[z][i-1][1]);
                }else if(G[y][i-1][0] > G[z][i-1][0]){
                    G[y][i][1] = max(G[y][i-1][1],G[z][i-1][0]);
                }
            }
            q.push(y);
        }
    }
}
// 在四个值中 筛选出最大给max1。严格次大给max2
void qu(ll &max1,ll &max2,ll x,ll y){
    if(max1 == x){
        max2 = max(max2,y);
    }else if(max1 < x){
        max2 = max(max1,y);
        max1  = x;
    }else if(max1 > x){
        max2 = max(max2,x);
    }
}
int lca(int x,int y,ll &max1,ll &max2){
    //  目标:找出x->y路径 最大和次大的值 
    if(deep[x] > deep[y] ) swap(x,y);
    for(int i=t;i>=0;i--){
        if(deep[f[y][i]] >= deep[x]) {
            qu(max1,max2,G[y][i][0],G[y][i][1]);
            y = f[y][i];
        }
    }
    if(x==y) return x;
    for(int i=t;i>=0;i--){
        if(f[x][i] != f[y][i]){
            //  一起跳
            qu(max1,max2,G[x][i][0],G[x][i][1]);
            qu(max1,max2,G[y][i][0],G[y][i][1]);
            //   更新x和 y;
            x = f[x][i];
            y = f[y][i];
        }
    }
    //  x和y需要再跳一步
    qu(max1,max2,G[x][0][0],G[x][0][1]);
    qu(max1,max2,G[y][0][0],G[y][0][1]);
    return f[x][0];
}
void solve(){
    int n,m;
    cin>>n>>m;
    // 初始化
    for(int i=1;i<=n;i++) fa[i] = i;
    for(int i=1;i<=m;i++){
        ll u,v,val;
        cin>>u>>v>>val;
        a[i] = (Node){u,v,0,val};
    }
    // 求t
    t = (int)(log(n)/log(2)) +1;
    krusal(n,m);
    bfs();
    ans = INF;
    for(int i=1;i<=m;i++){
        if(a[i].vis) continue;
        int x = a[i].u;
        int y = a[i].v;
        ll max1=-INF,max2=-INF;
        ll LCA = lca(x,y,max1,max2);
        // 这里我要明白一点 未在最小生成树的边,权值一定大于等于树上x->y路径边的权值.
        if(a[i].val==max1){
            ans = min(ans,sum - max2 + a[i].val);
        }else{
            ans = min (ans,sum - max1 + a[i].val);
        }
    }
    cout<<sum<<" "<<ans<<endl;
    // for(int i=1;i<=n;i++){
    //     for(int j=1;j<=5;j++){
    //         printf("(%lld,%lld) ",G[i][j][0],G[i][j][1]);
    //     }
    //     cout<<endl;
    // }
}
int main (){
  //freopen("in.txt","r",stdin);
  //freopen("out.txt","w",stdout);
  solve();
  getchar();
  getchar();
  return 0;
}

0x13 疫情控制 (倍增LCA +思维 +根到叶子检查点 )

疫情控制
题意:H国,n个城市,城市之间有n-1条双向道路互连构成树。1号城市是首都,也是根节点。
在树的叶子点是疫情扩散点(边境城市),现为了阻止扩散到首都,决定动用军队在一些城市建立检查点,使得从首都到边境城市的每一条路径上都至少有一个检查点,边境城市也可以建立检查点。注意首都不能建立检查点。
军队总数为 m 支。
1 一支军队可以在有道路连接的城市间移动,并在除首都以外的任意一个城市建立检查点,且只能在一个城市建立检查点。
2 一支军队经过一条道路从一个城市移动到另一个城市所需要的时间等于道路的长度(单位:小时)。
请问:最少需要多少个小时才能控制疫情?如果无法控制疫情则输出 −1
n个城市,m支军队。
思路:在此题中,给时间越多军队选择的越多更有可能控制疫情。符合二分的思想。二分答案。给了时间只要得出能与不能控制疫情就行了。
军队可以分为两类:
1.第一类在mid小时内无法到达根节点,这些结点就尽量往根节点的方向移动。处理完第一类的军队。
记根节点的子节点集合为son(root),对每个 s ∈ s o n ( r o o t ) s\in son(root) sson(root),统计还有叶子节点尚未被管辖的点,记为集合H。
2.第二类mid内能到到根节点的军队
在这里插入图片描述
所以对任意 x ∈ H x\in H xH并且s上rest值最小的军队不足以移动到根再返回s的这支军队驻扎在s,即是管辖s为根的子树。
原理说完了,该这么实现?
H集合怎么找到呢? 用fg[结点] = 有无军队. 使用dfs自底向上回溯,fg[u]是1的前提示u的所有儿子节点都必须fg[v] =1;
然后下一步是删除部分的H集合的s(这一步是为了后续剩余的军队移至根节点,再跳往H集合的s)。
并将剩下的军队移步至根节点,对此时军队的rest排序和对根到s距离排序。使用双指针,只有H集合的s全部指完,疫情得到控制可行
,否则,不可行。
注意: 路径权值 w < = 1 e 9 w<=1e9 w<=1e9,开 long long
ACcode详解:

#include<bits/stdc++.h>
#define ll long long
#define ld long double
#define ull unsigned long long
#define rep(i,a,b) for(int i=a;i<=b;i++)
ll gcd(ll a,ll b){ return b? gcd(b,a%b):a;}
const int N=1e5+10;
const ll P=1e9+7;
ll read(){
    ll s = 0, f = 1; char ch = getchar();
    while(!isdigit(ch)){
        if(ch == '-') f = -1;
        ch = getchar();
    }
    while(isdigit(ch)) s = (s << 3) + (s << 1) + (ch ^ 48), ch = getchar();
    return s * f;
}
using namespace std;
struct E{ ll to,next,val;} Edge[N];
int tot,head[N];
void AddEdge(int u,int v,int val){
    Edge[++tot] = (E){v,head[u],val};
    head[u] = tot;
}
ll w[N],deep[N],f[N][30];  // w:根到i的权值
int t = 20;
int n,m;
void bfs(){
    queue<int> q;
    q.push(1);
    f[1][0] = 0;
    deep[1] =1;
    deep[0] =1;
    while(q.size()){
        int u = q.front();
        q.pop();
        for(int i=head[u];i;i=Edge[i].next){
            int v= Edge[i].to;
            if(deep[v]) continue;
            q.push(v);
            deep[v] = deep[u]+1;
            w[v] = w[u] + Edge[i].val;
            f[v][0] = u;
            for(int i=1;i<=t;i++){
                f[v][i] = f[f[v][i-1]][i-1];
            }
        }
    }
}
struct Node{ ll x,res;} p[N];
bool cmp(Node a,Node b){
    return a.res<b.res;
}
int a[N];
int fg[N];  // 为1有检查点
void search(int u,int f){   // 筛选出H集合,根的儿子节点是0,节点属于H集合
    if(fg[u]==1) return ;
    int x = 0;
    int y  =0;
    for(int i=head[u];i;i=Edge[i].next){
        int v= Edge[i].to;
        if(v==f) continue;
        x++;
        search(v,u);
        if(fg[v]==1) y++;
    }
    if(x==y && x>=1) fg[u] = 1;
}
bool check(ll time){
    //  初始化 
    memset(fg,0,sizeof(fg));
    int cnt = 0;
    for(int i=1;i<=m;i++){
        int x = a[i];
        ll res = time;
        for(int j=t;j>=0;j--){
            if(deep[f[x][j]]>1 && res>=w[x]-w[f[x][j]]){
                res -= (w[x] - w[f[x][j]]);
                x = f[x][j];
            }
        }
        if(deep[x]==2) {
            //加入
            p[cnt++] = (Node){x,res};
        }else fg[x] = 1;        
    }
    // 选出H 集合
    search(1,0);
    // 注意一些小小细节,有可能一个节点有多个军队呢,但是军队当前的可用时间不一。
    // 所以距离最小的优先考虑
    sort(p,p+cnt,cmp);
    for(int i=0;i<cnt;i++){   // 删除部分H, 即是fg[u] = 1;
        if(fg[p[i].x]==0 && w[p[i].x]*2 >= p[i].res) {
            fg[p[i].x] = 1;
            p[i].res = 0;  // 待在原地就好,所以可用时间记为0
        }else{
            p[i].res -=w[p[i].x]; //移步到根节点
        }
    }
    sort(p,p+cnt,cmp);
    // 筛选出 没走到的根子节点
    vector<pair<ll,int> > H; // 距离 节点
    for(int i=head[1];i;i=Edge[i].next){
            int v= Edge[i].to;
            if(v==0) continue;
            if(fg[v]==0) H.push_back(make_pair(w[v],v));
    }
    sort(H.begin(),H.end());
    //  双指针
    int i=0,j=0;
    while(i<cnt && j<H.size()){
        if(p[i].res < H[j].first) i++;
        else {
            i++;
            j++;
        }
    }
    if(j==H.size()) return true;
    else return false;
}
void solve(){
    cin>>n;
    ll l = 0,r= 0;
    for(int i=1;i<n;i++){
        ll u,v,val;
        cin>>u>>v>>val;
        r+=val;
        AddEdge(u,v,val);
        AddEdge(v,u,val);
    }
    cin>>m;
    for(int i=1;i<=m;i++) cin>>a[i];
    bfs();
    // for(int i=1;i<=n;i++){
    //     cout<<i<<":";
    //     for(int j=0;j<=6;j++){
    //         printf("(%d) ",f[i][j]);
    //     }
    //     cout<<endl;
    // }
    ll ans =  -1;
    while(l<=r){
        ll mid = (l+r)>>1;
        if(check(mid)){
            ans = mid;
            r = mid -1;
        }else{
            l = mid + 1;
        }
    }
    cout<<ans<<endl;
}
int main (){
  //freopen("in.txt","r",stdin);
  //freopen("out.txt","w",stdout);
  solve();
  getchar();
  getchar();
  return 0;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

axtices

谢谢您的打赏

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值