【好题选讲】图论阴间题选讲 III (图的连通性与缩点)

P2515 [HAOI2010] 软件安装

题意
给定 \(N\) 个软件,每个软件有:

  • 空间消耗 \(W_i\)
  • 价值 \(V_i\)
  • 依赖关系 \(D_i\)\(D_i=0\) 表示无依赖,每个软件仅有一个依赖)

选择若干软件安装到容量为 \(M\) 的磁盘中,满足:

  1. 若安装软件 \(i\),则必须安装其依赖链上的所有软件
  2. 最大化总价值 \(\sum V_i\)

数据范围

  • 软件数 \(N \leq 100\)
  • 磁盘容量 \(M \leq 500\)
  • 空间消耗 \(0 \leq W_i \leq M\)
  • 软件价值 \(0 \leq V_i \leq 1000\)

如果依赖关系是棵树我们是会做的,是个简单的树上背包,但是本题的依赖关系可能不是树。

我们想缩点,首先改变一下连边方式,我们把点 \(i\) 从它依赖的点 \(D_i\) 向它连边,这样一条有向边 \(i,j\) 就可以代表 \(i\) 约束 \(j\)。下面对这个有向图缩点,这样有个性质就是在同一个 SCC 里的点一定是必须同时被安装的,因为同一个 SCC 里面的所有点都可以互相到达,意味着每个点都被其中的一个点依赖了。我们把各个 SCC 当成一个大物品。

改变连边方式的作用体现在这里:此时入度为 \(0\) 的节点代表没有约束的点,而且由于每个点只会有至多 \(1\) 个约束,意味着缩点完成后的图一定是一棵树,父指向子的边代表必须选择了父亲才能去选儿子,这正好是树上背包问题的约束。

于是在缩点后的图上进行树上背包即可。

code

Show me the code


P4819 [中山市选] s人游戏

题意
给定 \(N\) 个点和 \(M\) 条有向边构成的关系图,其中:

  • 查问一个平民点可获得其所有邻点的身份信息
  • 查问杀手点会被杀害

求最优查问策略下,能确定杀手身份且不被杀的最大概率

数据范围

  • 点数 \(N \leq 10^5\)
  • 边数 \(M \leq 3 \times 10^5\)
  • 要求输出保留6位小数

看到概率别害怕

首先由于题目要求确定杀手身份,这意味我们必须知道所有人的身份信息。

如果我们访问了一个平民,我们可以得到其所有邻点的身份信息,这让我们可以一路访问下去,直到找到杀手。

什么叫一路访问下去呢?我们给原图进行强连通分量缩点,在同一个 SCC 中的人因为可以到达同点中的所有人,也就是如果我们知道了这人的身份,整个 SCC 中人的身份我们都可以知道。这些人可能还知道其它 SCC 中人的身份。这样,如果我们访问缩完点后 DAG 上一个 SCC 的其中一个人,这个 SCC 在 DAG 上可以到达的所有 SCC 我们都能知道身份了。

为了让受益最大,只访问所有入度为 \(0\) 的 SCC 的其中任意一人就可以了。即:我们只需要开入度为 \(0\) 的 SCC 的个数次盲盒,就可以确定所有人的身份。

还没完,考虑这样的特殊情况:如果存在一个入度为 \(0\) 的 SCC,但是这个 SCC 只有一个人,且这个人连向的 SCC 也有别的入度为 \(0\) 的 SCC 连向(即:我们不需要访问这个大小为 \(1\) 的 SCC,也可以知道它连向的 SCC 的身份)。那么我们大可以不访问这个这个大小为 \(1\) 的 SCC 的人,因为我们可以知道除了这个 SCC 意外其它所有 SCC 的身份,此时可以使用排除法知道这个 SCC 的身份,因为只有一个人。

特判下这种情况即可通过此题。

code

Show me the code
#define psb push_back
#define mkp make_pair
#define rep(i,a,b) for( int i=(a); i<=(b); ++i)
#define per(i,a,b) for( int i=(a); i>=(b); --i)
#define rd read()
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
ll read(){
  ll x=0,f=1;
  char c=getchar();
  while(c>'9'||c<'0'){if(c=='-') f=-1;c=getchar();}
  while(c>='0'&&c<='9'){x=(x<<3)+(x<<1)+(c^48);c=getchar();}
  return x*f;
}
const int N=1e6;
struct e{
  int u,v,nxt;
}edge[N];
int _head[N];
int cnt=-1;
void add(int u,int v){
  cnt++;
  edge[cnt].u=u;
  edge[cnt].v=v;
  edge[cnt].nxt=cnt;
  edge[cnt].nxt=_head[u];
  _head[u]=cnt;
  return ;
}
int dfn[N],low[N];
bool vis[N];int idx=0;
stack<int> s;
int scc=0;
int col[N];int siz[N];
void tarjan(int u){
  dfn[u]=low[u]=++idx;
  vis[u]=1;
  s.push(u);
  for(int i=_head[u];i!=-1;i=edge[i].nxt){
    int v=edge[i].v;
    if(!dfn[v]){
      tarjan(v);
      low[u]=min(low[u],low[v]);
    }
    else if(vis[v]){
      low[u]=min(low[u],dfn[v]);
    }
  }
  if(dfn[u]==low[u]){
    scc++;
    while(s.top()!=u){
      int v=s.top();s.pop();
      col[v]=scc;siz[scc]++;
      vis[v]=0;
    }
    s.pop();vis[u]=0;
    col[u]=scc;siz[scc]++;
  }
  return ;
}
vector<int > ne[N];
int inner[N];
int main(){
  
  memset(_head,-1,sizeof _head);
  int n,m;
  cin>>n>>m;
  for(int i=1;i<=m;i++){
    int u,v;cin>>u>>v;
    add(u,v);
  }
  for(int i=1;i<=n;i++){
    if(!dfn[i])tarjan(i);
  }
  map<pair<int,int>,bool> mp; 
  for(int i=1;i<=n;i++){
    for(int j=_head[i];j!=-1;j=edge[j].nxt){
      int v=edge[j].v;
      if(col[v]==col[i])continue;
      if(mp.find(mkp(col[i],col[v]))!=mp.end())continue;
      ne[col[i]].push_back(col[v]);
      inner[col[v]]++;
      mp[mkp(col[i],col[v])]=1;
    }
  }
  bool s=1;int cct=0;int toa=0;bool htt=1;
  for(int i=1;i<=scc;i++){
    if(inner[i]!=0)continue;
    cct++;bool f=1;
    if(siz[i]==1){
      for(int j=0;j<ne[i].size();j++){
        if(inner[ne[i][j]]<=1){f=0;break;}
      }
      if(f&&s){
        if(htt==0){s=0;}
        else {cct--;htt=0;}
      }
    }
  }
  cout<<fixed<<setprecision(6)<<1.0*(n-cct)/n;
  
  return 0;
}

P5025 [SNOI2017] 炸弹

题意
给定数轴上 \(n\) 个炸弹的位置 \(x_i\)(严格递增)和爆炸半径 \(r_i\)。定义:

  • 炸弹 \(i\) 爆炸会引爆满足 \(|x_j - x_i| \leq r_i\) 的所有炸弹 \(j\)

炸弹的爆炸具有传递性。

要求计算:

\[\sum_{i=1}^n \left( i \times \text{能被炸弹 }i\text{ 引爆的炸弹数量} \right) \mod 10^9+7 \]

数据范围

  • 炸弹数量 \(n \leq 5 \times 10^5\)
  • 坐标范围 \(-10^{18} \leq x_i \leq 10^{18}\)
  • 爆炸半径 \(0 \leq r_i \leq 2 \times 10^{18}\)

初见就想到了线段树优化建图,于是暴力给值域上动态开点权值线段树,对每个炸弹向它对应能炸到的区间连边。完成后再进行一个点的缩,在同一个 SCC 中的炸弹,其中任何一个引爆都会一次引爆其它的炸弹,也会连锁引爆下面链接的 SCC 的所有炸弹。

但是你会发现统计一个 SCC 下面引得 SCC 中有多少个炸弹是不好用拓扑排序做的,而且直接对值域开线段树会炸空间。

以下是动态开点权值线段树的错误代码:

Show me the code
#define psb push_back
#define mkp make_pair
#define rep(i,a,b) for( int i=(a); i<=(b); ++i)
#define per(i,a,b) for( int i=(a); i>=(b); --i)
#define rd read()
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
ll read(){
  ll x=0,f=1;
  char c=getchar();
  while(c>'9'||c<'0'){if(c=='-') f=-1;c=getchar();}
  while(c>='0'&&c<='9'){x=(x<<3)+(x<<1)+(c^48);c=getchar();}
  return x*f;
}
const long long lefte=0,righte=(long long)1e18 * 2;
const int N=1e6;
struct seg{
  int ls,rs;  
  bool isleaf;
}t[N];
#define def seg{0,0,0}
int idx=1;
vector<int> edge[N];
int leaf[N],reff[N];
ll ori=0;
void bc(int p,ll l,ll r,ll pos,int id){
  if(l==pos&&pos==r){
    leaf[id]=p;
    t[p].isleaf=1;
    return;
  }
  ll mid=l+r>>1;
  
  if(pos<=mid){
    if(t[p].ls==0){
      t[p].ls=++idx;
      edge[p].push_back(idx);
      bc(t[p].ls,l,mid,pos,id);
    }
    else
    	bc(t[p].ls,l,mid,pos,id);
  }
  if(mid<pos){
    if(t[p].rs==0){
      t[p].rs=++idx;
      edge[p].push_back(idx);
      bc(t[p].rs,mid+1,r,pos,id);
    }
    else 
    	bc(t[p].rs,mid+1,r,pos,id);
  }
  return ;
}
const int NM=500005;
ll rad[NM],posi[NM];
int dfn[N],low[N];
bool vis[N];
stack<int> s;
int id=0;
int col[N];int val[N];
int scc=0;
void tarjan(int u){
  dfn[u]=low[u]=++id;
  vis[u]=1;
  s.push(u);
  for(int i=0;i<edge[u].size();i++){
    int v=edge[u][i];
    if(!dfn[v]){
      tarjan(v);
      low[u]=min(low[u],low[v]);
    }
    else if(vis[v])
      low[u]=min(low[u],dfn[v]);
  }
  if(dfn[u]==low[u]){
    scc++;
    while(s.top()!=u){
      int v=s.top();
      s.pop();
      col[v]=scc;vis[v]=0;
      if(t[v].isleaf)val[scc]++;
    }
    col[u]=scc;vis[u]=0;
    if(t[u].isleaf)val[scc]++;
    s.pop();
  }
  return ;
}
void ffm(int p,ll l,ll r,ll tl,ll tr,int fromi){
  if(tl<=l&&r<=tr){
    edge[fromi].push_back(p);
    return;
  }
  ll mid=l+r>>1;
  
  if(tl<=mid)
    if(t[p].ls!=0)
    	ffm(t[p].ls,l,mid,tl,tr,fromi);
  }
  if(mid<tr)
    if(t[p].rs!=0)
    	ffm(t[p].rs,mid+1,r,tl,tr,fromi);
  
  return ;
}
const long long mod=1e9+7;
vector<int> nw[N];
int inner[N];
int wi[N];
int main(){
  
  int n;
  cin>>n;
  t[0]=def;
  t[1]=def;
  for(int i=1;i<=n;i++){
    ll p,r;cin>>p>>r;
    p+=(long long)1e18;
    rad[i]=r;posi[i]=p;
    bc(1,lefte,righte,p,i);
  }
  for(int i=1;i<=n;i++)
    ffm(1,lefte,righte,llabs(posi[i]-rad[i]),llabs(posi[i]+rad[i]),leaf[i]);
  for(int i=1;i<=idx;i++){
    if(!dfn[i])tarjan(i);
  }
  map<pair<int,int>,bool > ma;
  for(int i=1;i<=idx;i++){
    for(int j=0;j<edge[i].size();j++){
      int v=edge[i][j];
      if(col[v]==col[i]||ma.find(mkp(col[v],col[i]))!=ma.end())continue;
      nw[col[v]].push_back(col[i]);
      ma[mkp(col[v],col[i])]=1;
      inner[col[i]]++;
    }
  }
  queue<int> sa;
  for(int i=1;i<=scc;i++){
    if(inner[i]==0)sa.push(i);
    wi[i]=val[i];
  }
  while(sa.size()){
    int u=sa.front();
    sa.pop();
    for(int i=0;i<nw[u].size();i++){
      int v=nw[u][i];
      wi[v]+=val[u];
      inner[v]--;
      if(inner[v]==0){
        sa.push(v);
      }
    }
  }
  ll ans=0;
  for(int i=1;i<=n;i++){
    int color=col[leaf[i]];
    ans=(ans+i*(wi[color])%mod)%mod;
  }
  cout<<ans;
  
  return 0;
}

先来解决炸空间的问题,利用离散化的思想,我们按照位置对这些炸弹排序(这里输入给的就是有序的),通过二分,我们可以找到每个炸弹爆炸可以引爆的炸弹的范围。

我们直接对炸弹建立完整的线段树,向引爆炸弹的范围连边即可极大的降低空间消耗。

接下来怎样正确统计引爆炸弹数呢?首先引爆炸弹连锁爆炸的范围一定是个连续的区间,我们自然关心这个区间的左右端点。这个在缩完点后的 DAG 上就是一个 SCC 自己及其引出的 SCC 中炸弹坐标的最大最小值。这个用拓扑排序统计是好做的。于是处理下每个 SCC 能到的炸弹坐标的最大最小值,二分可以得到对应在炸弹上的区间,统计数量即可。

以下是正确代码:

Show me the code
#define psb push_back
#define mkp make_pair
#define rep(i,a,b) for( int i=(a); i<=(b); ++i)
#define per(i,a,b) for( int i=(a); i>=(b); --i)
#define rd read()
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
ll read(){
  ll x=0,f=1;
  char c=getchar();
  while(c>'9'||c<'0'){if(c=='-') f=-1;c=getchar();}
  while(c>='0'&&c<='9'){x=(x<<3)+(x<<1)+(c^48);c=getchar();}
  return x*f;
}
const int N=5e5+5;
struct b{
  ll pos,rad;
  int id;
  b():pos(0),rad(0),id(0){}
  b(ll x):pos(x),rad(0),id(0){}
};
b bo[N];
ll pos[N],rad[N];
bool cmp(b x,b y){return x.pos<y.pos;}
struct tr{
  int l;int r;bool isleaf;
}t[N*4];
int leaf[N],dfn[N*4],low[N*4];
bool vis[N*4];
stack<int> s;
vector<int> edge[N*4];
int pallc=0;
void build(int p,int l,int r){
  pallc=max(p,pallc);
  t[p].l=l;t[p].r=r;
  if(l==r){t[p].isleaf=1;leaf[l]=p;return ;}
  int mid=l+r>>1;
  build(p*2,l,mid);
  build(p*2+1,mid+1,r);
  edge[p*2].push_back(p);
  edge[p*2+1].push_back(p);
  return ;
}
void bc(int p,int l,int r,int id){
  if(l<=t[p].l&&t[p].r<=r){
    edge[p].push_back(id);
    return ;
  }
  int mid=t[p].l+t[p].r>>1;
  if(l<=mid)bc(p*2,l,r,id);
  if(mid<r)bc(p*2+1,l,r,id);
  return ;
}
int idx=0,scc=0;
int col[N*4];
ll pmin[N*4],pmax[N*4];
int inner[N*4];
vector<int> nedge[N*4];
int coref[N];
void tarjan(int u){
  dfn[u]=low[u]=++idx;
  s.push(u);
  vis[u]=1;
  for(int i=0;i<edge[u].size();i++){
    int v=edge[u][i];
    if(!dfn[v]){
      tarjan(v);
      low[u]=min(low[v],low[u]);
    }
    else if(vis[v])
      low[u]=min(low[u],dfn[v]);
  }
  if(dfn[u]==low[u]){
    scc++;
    while(s.top()!=u){
      int v=s.top();
      s.pop();
      col[v]=scc;vis[v]=0;
      if(t[v].isleaf){
        pmin[scc]=min(pmin[scc],pos[t[v].l]);
        pmax[scc]=max(pmax[scc],pos[t[v].l]);
        coref[t[v].l]=scc;
      }
    }
    s.pop();
    col[u]=scc;vis[u]=0;
    if(t[u].isleaf){
      pmin[scc]=min(pmin[scc],pos[t[u].l]);
      pmax[scc]=max(pmax[scc],pos[t[u].l]);
      coref[t[u].l]=scc;
    }
  }
}
const long long mod=1e9+7;
signed main(){
  
  int n;cin>>n;
  fill(pmin,pmin+N*4-1111,LONG_LONG_MAX);
  fill(pmax,pmax+N*4-1111,LONG_LONG_MIN);
  for(int i=1;i<=n;i++){
    cin>>bo[i].pos>>bo[i].rad;
    pos[i]=bo[i].pos;
    rad[i]=bo[i].rad;
    bo[i].id=i;
  }
  sort(bo+1,bo+1+n,cmp);
  build(1,1,n);
  for(int i=1;i<=n;i++){
    b lf(bo[i].pos-bo[i].rad);
    b ri(bo[i].pos+bo[i].rad);
    int bl=lower_bound(bo+1,bo+1+n,lf,cmp)-bo;
    int br=upper_bound(bo+1,bo+1+n,ri,cmp)-bo-1;
    bc(1,bl,br,leaf[i]);
  }
  for(int i=1;i<=pallc;i++)
    if(!dfn[i])tarjan(i);
  for(int i=1;i<=pallc;i++){
    for(int j=0;j<edge[i].size();j++){
      int v=edge[i][j];
      if(col[v]==col[i])continue;
      nedge[col[i]].push_back(col[v]);
      inner[col[v]]++;
    }
  }
  queue<int> q;
  for(int i=1;i<=scc;i++)
    if(inner[i]==0)q.push(i);
  while(q.size()){
    int u=q.front();
    q.pop();
    for(int i=0;i<nedge[u].size();i++){
      int v=nedge[u][i];
      pmax[v]=max(pmax[v],pmax[u]);
      pmin[v]=min(pmin[v],pmin[u]);
      inner[v]--;
      if(inner[v]==0)q.push(v);
    }
  }
  ll ans=0;
  for(int i=1;i<=n;i++){
    b lf(pmin[coref[i]]);
    b ri(pmax[coref[i]]);
    int bl=lower_bound(bo+1,bo+1+n,lf,cmp)-bo;
    int br=upper_bound(bo+1,bo+1+n,ri,cmp)-bo-1;
    ans=(ans+1ll*(br-bl+1)*i%mod)%mod;
  }
  cout<<ans;
  
  return 0;
}

P5058 [ZJOI2004] 嗅探器

题意
给定无向图 \(G=(V,E)\) 和两个关键节点 \(a,b\),求满足以下条件的最小节点 \(v\)

  • \(v \neq a,b\)
  • 任意 \(a\)\(b\) 的路径都经过 \(v\)

数据范围

  • 节点数 \(|V|=n\)\(1 \leq n \leq 2 \times 10^5\)
  • 边数 \(|E| \leq 5 \times 10^5\)

把图的所有割点找出来是容易的,问题的关键在于判断这个割点能否把 \(a,b\) 分开。

这里有个很好玩的小技巧,对原来找点双的 tarjan 做些小改动即可。

我们特别选择 \(a\)\(b\) 也可以)开始 tarjan。如果 tarjan 还没有找到 \(b\),此时 \(dfn_b\) 一定是 \(0\)。这个是显然的。

如果我们判定了一个点 \(u\) 是割点,且我们已经找过了 \(u\) 的邻接点 \(v\)\(dfn_b\) 依然是 \(0\),这样我们就可以判断,\(b\) 不在 \(u\)\(v\) 的这一系列点双里面。

同理的,如果找过了 \(u\) 的邻接点 \(v\)\(dfn_b\) 不再是 \(0\),那么 \(b\) 就在 \(u\)\(v\) 的这一系列点双里面。此时割点 \(u\) 就可以将 \(a,b\) 分开了。

以上所有的步骤只需一句在 tarjan 中的

if(dfn[v]<=dfn[b]&&u!=a)ans=min(ans,u);

同理的,如果你要判断边双里面的一个桥能不能分开两个点,也可以用这样的方法。

code

Show me the code
#define psb push_back
#define mkp make_pair
#define rep(i,a,b) for( int i=(a); i<=(b); ++i)
#define per(i,a,b) for( int i=(a); i>=(b); --i)
#define rd read()
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
ll read(){
  ll x=0,f=1;
  char c=getchar();
  while(c>'9'||c<'0'){if(c=='-') f=-1;c=getchar();}
  while(c>='0'&&c<='9'){x=(x<<3)+(x<<1)+(c^48);c=getchar();}
  return x*f;
}
const int N=2e5+5;
const int M=1e6+1e5;
struct e{
  int u;int v;int nxt;
}edge[M];
int _head[N];int cnt=-1;
void add(int u,int v){
  cnt++;
  edge[cnt].u=u;
  edge[cnt].v=v;
  edge[cnt].nxt=_head[u];
  _head[u]=cnt;
  return ;
}
int dfn[N],low[N];
stack<int> s;
int idx=0;
int a,b;
int ans=73357733;
void tarjan(int u,int id){
  dfn[u]=low[u]=++idx;
  s.push(u);
  for(int i=_head[u];i!=-1;i=edge[i].nxt){
    int v=edge[i].v;
    if(id==(i^1))continue;
    if(!dfn[v]){
      tarjan(v,i);
      low[u]=min(low[u],low[v]);
      if(dfn[u]<=low[v]){
        if(dfn[v]<=dfn[b]&&u!=a)ans=min(ans,u);
      }
    }
    else low[u]=min(low[u],dfn[v]);
  }
}
int main(){
 
  int n;cin>>n;
  int u,v;
  memset(_head,-1,sizeof _head);
  while(cin>>u>>v){
    if(u==0&&v==0)break;
    add(u,v);add(v,u);
  }
  cin>>a>>b;
  tarjan(a,-1);
  if(ans==73357733||overall)cout<<"No solution";
  else cout<<ans;

  return 0;
}
posted @ 2025-05-25 12:08  hm2ns  阅读(14)  评论(0)    收藏  举报