2025 端午集训 Day 4 阴间题选记

闲话

图论题还是比 DS 要好玩的。

单源次短路

为什么考纲里多了个这个。

单源次有两种,可以重复经过边的和不能重复经过边的。

啊当然还有严格次短和非严格次短。这个东西好说。

对于前者,我们定义次短路数组 \(his_i\) 表示从源到 \(i\) 的次短路。

这个东西就和次大值一样更新就行了。

具体来说,在松弛操作时,如果对应的点无法被松弛,那么就尝试松弛它的次短路。

如果对应的点可以被松弛,那么原来的最短路自动成为次短路。

但是这里要注意次短路也是有可能去更新次短路的。

于是如果次短路被松弛了也要加入优先队列里面去。

然后有一个板子,这个板子要求严格可重复路径次短:

P2865 [USACO06NOV] Roadblocks G

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=5005;
struct ed{
  int u,v,w;
  bool operator<(const ed &x)const{
    return x.w<w;
  }
};
vector<ed> e[N];
int dis[N],his[N];
void dij(int s){
  priority_queue<ed> q;
  q.push(ed{s,0,0});
  memset(dis,0x3f,sizeof dis);
  memset(his,0x3f,sizeof his);
  dis[s]=0;
  while(q.size()){
    int u=q.top().u;int w=q.top().w;
    q.pop();
    for(auto ei:e[u]){
      int v=ei.v,wi=ei.w;
        if(dis[v]>w+wi){
          his[v]=dis[v];
          dis[v]=w+wi;
          q.push(ed{v,0,dis[v]});
        }
        if(his[v]>w+wi&&dis[v]<w+wi){
          his[v]=w+wi;
          q.push(ed{v,0,his[v]});
        }
    }
  }
}
int main(){
  
  int n,r;
  cin>>n>>r;
  for(int i=1;i<=r;i++){
    int u,v,w;cin>>u>>v>>w;
    e[u].push_back(ed{u,v,w});
    e[v].push_back(ed{v,u,w});
  }
  dij(1);
  cout<<his[n];

  return 0;
}

如果是非严格的,把 dij for 里面的第二个 if 中的条件改成 his[v]>w+wi&&dis[v]<=w+wi 就行。


如果次短路不可以重复经过,那么我们暴力删边跑最短路。

当然也不是乱删边,我们首先对全图跑一个最短路,建出最短路树(DAG),于是我们只删最短路树上面的边即可。

每删一条边就重新一次最短路,这时候记录的答案

当然也有一个板子:

P1491 集合位置

这题数据水啊,直接暴力删边就行。要求非严格不可重边次短路。

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;
}
struct po{
  int x,y;
}p[500];
double d(int x,int y){
  return sqrt(1.0*abs(p[x].x-p[y].x)*abs(p[x].x-p[y].x)+abs(p[x].y-p[y].y)*abs(p[x].y-p[y].y)*1.0);
}
struct ed{
  int u,v;
  double w;
  int nxt;
  bool operator<(const ed &x)const{
    return x.w<w;
  }
}edge[250000];
int _head[500];int idx=-1;
void add(int u,int v){
  idx++;edge[idx].u=u;edge[idx].v=v;edge[idx].w=d(u,v);
  edge[idx].nxt=_head[u];_head[u]=idx;
  return ;
}
int vis[500];double dis[500];
int ban=-1;
void dij(int s){
  for(int i=1;i<=400;i++)dis[i]=3737373737;
  memset(vis,0,sizeof vis);
  priority_queue<ed> q;
  q.push(ed{0,s,0,0});
  while(q.size()){
    ed x=q.top();q.pop();
    int u=x.v;double wii=x.w;
    if(vis[u])continue;
    vis[u]=1;
    dis[u]=wii;
    for(int i=_head[u];i!=-1;i=edge[i].nxt){
      if(i==ban||(i^1)==ban)continue;
      int v=edge[i].v;double w=edge[i].w;
      if(dis[v]>dis[u]+w){
        dis[v]=dis[u]+w;
        q.push(ed{0,v,dis[v],0});
      }
    }
  }
}
int main(){
  
  memset(_head,-1,sizeof _head);
  int n,m;cin>>n>>m;
  for(int i=1;i<=n;i++){
    cin>>p[i].x>>p[i].y;
  }
  for(int i=1;i<=m;i++){
    int u,v;cin>>u>>v;
    add(u,v);add(v,u);
  }
  dij(1);
  double topwater=dis[n];
  double ans=73357733;
  for(int i=0;i<=idx;i+=2){
    ban=i;
    dij(1);
    if(fabs(dis[n]-topwater)<=1e-6)continue;
    else ans=min(ans,dis[n]);
  }
  if(fabs(ans-73357733)<=1e-6)cout<<-1;
  else cout<<fixed<<setprecision(2)<<ans;

  return 0;
}

严格次短的话就是如果删边以后跑出来最短路和不删边的跑出来一样就忽略这个最短路。

圆方树科技

学边双点双的时候很头疼一个点就是点双没办法缩点成一棵树或一个 DAG。。

所以有了圆方树,它能帮我们把点双变成一棵树。

构造它很简单,给每个点双开一个新点,与各点双内的点连边,把原来两点之间的边都去掉,于是这个东西就会变成一棵树。

为了让新点和原来的点有点区分,我们让新开的点是方形的,原来的点是圆形的,于是这就是圆方树。

显然这个形状什么用都没有。。

实现上通常给新点分配 \(n+1,n+2 \cdots\) 这样的编号,于是直接用点的编号判断新旧点就行了。

来几个题看看圆方树有什么好性质。

P3469 [POI 2008] BLO-Blockade

结论:如果无向图中一个点不是割点,那么这个点一定是圆方树上的叶子。

这个很显然吧。。

然后就是统计删去一个点之后会在圆方树上造出许多连通块。计算下这些连通块的权值,两两乘一下。

但是这样容易被卡到 \(O(n^2)\),例如在一个菊花图上,我们要优化。

先把式子写出来,设除了自己这个点之外分成了 \(k\) 个连通块,各连通块的大小是 \(s_i\),那么我们要求:

\[\sum_{i=1}^{k}\sum_{j=1,i\ne j}^{k} s_i \times s_j \]

考虑固定 \(i\),计算 \(\sum_{j=1,i\ne j}^{k} s_j\) 贡献了多少次 \(s_i\)

设总点数为 \(n\),那么显然有 \(\sum_{j=1,i\ne j}^{k} s_j = n-s_i-1\)

于是原来的式子变成:

\[\sum_{i=1}^{k} s_i \times (n-s_i-1) \]

这样就舒服了啊。

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;
struct lsqxx{
  int u,v,nxt;
}e[N*6];
int id=-1;
int _head[N];
void add(int u,int v){
  id++;
  e[id].u=u;e[id].v=v;
  e[id].nxt=_head[u];
  _head[u]=id;
  return;
}
int dfn[N],low[N];
vector<int> col[N];
stack<int> s;
int idx=0;
int vdcc=0;
void tarjan(int u,int fa){
  dfn[u]=low[u]=++idx;
  s.push(u);
  for(int i=_head[u];~i;i=e[i].nxt){
    int v=e[i].v;
    if(fa==(i^1))continue;
    if(!dfn[v]){
      tarjan(v,i);
      low[u]=min(low[u],low[v]);
      if(dfn[u]<=low[v]){
        vdcc++;
        col[vdcc].push_back(u);
        while(s.top()!=v){
          int vv=s.top();s.pop();
          col[vdcc].push_back(vv);
        }
        s.pop();
        col[vdcc].push_back(v);
      }
    }
    else{
      low[u]=min(low[u],dfn[v]);
    }
  }
}
vector<int> edge[N];
ll siz[N];int n,m;
vector<int> cp;
int dep[N];
void dfs1(int u,int fa){
  bool inner=0;
  for(int v:edge[u]){
    if(v==fa)continue;
		inner=1;
    dep[v]=dep[u]+1;
    dfs1(v,u);
    siz[u]+=siz[v];
  }
  if(u<=n&&inner)cp.push_back(u);
  if(u<=n) siz[u]++;
  return ;
}
ll ans[N];
int main(){
  
  memset(_head,-1,sizeof _head);
  cin>>n>>m;
  for(int i=1;i<=m;i++){
    int u,v;cin>>u>>v;
    add(u,v);add(v,u);
  }
  tarjan(1,-1);
  for(int i=1;i<=vdcc;i++){
    for(int v:col[i]){
      edge[i+n].push_back(v);
      edge[v].push_back(i+n);
      //cout<<i+n<<' '<<v<<'\n';
    }
  }
  dep[1+n]=0;
  dfs1(1+n,-1);
  for(auto v:cp){
    vector<ll> gcc;
    int tot=n-1;
    for(int vi:edge[v]){
      if(dep[vi]<dep[v])continue;
      gcc.push_back(siz[vi]);
      tot-=siz[vi];
    }
    gcc.push_back(tot);
    for(int i=0;i<gcc.size();i++){
      ans[v]+=(n-1-gcc[i])*gcc[i];
    }
  }
  for(int i=1;i<=n;i++){
  	if(ans[i]==0){cout<<2*n-2<<'\n';}
		else cout<<ans[i]+2*n-2<<'\n';
  }
  
  return 0;
}

P4630 [APIO2018] 铁人两项

这题有点复杂。

首先我们当然想固定 \(s,f\)\(c\),我们大胆猜想:\(c\) 可以选 \(s,f\) 和从 \(s\)\(f\) 经过的点双中的所有点。

事实上我们猜对了。。

我们有点双的一个非常好的性质:对于一个点双中的两点,它们之间简单路径的并集,恰好完全等于这个点双。

证明不是很会啊。。

这个性质告诉我们,如果 \(s,t\) 在同一个点双连通分量中,每一条边都可以存在于这两点之间的简单路径上,也就意味着每一点都可以存在于这两点的简单路径上,我们当然可以选择这个点双中除了这两个点之外的所有点。

扩展到两点不在同一个点双中,考虑从 \(s\)\(f\) 经过的其中一个点双,在圆方树上考虑,两点会各自到达这个点双的一个割点,这个路径肯定是简单路径了。然后就是这两个割点在这个点双中的所有简单路径,再次应用上面的性质,我们也可以得出我们可以选择这个点双中的所有点。这样,我们就证明了我们可以选 \(s,f\) 和从 \(s\)\(f\) 经过的点双中的所有点。

接下来我们要做的是:对于树上的任意不相等节点对,计算它们的简单路径上经过的点双中点的数量之和再减二。

当然不能枚举所有点对,但是我问你,如果只让你计算一个点对,你该怎么办?

要在圆方树上考虑。这里是圆方树的第二个小技巧,计数时给圆方树上的点赋权值。这里,我们把所有方点的权值设为所在点双连通分量的大小,所有圆点的权值设为 \(-1\)

这样,计算树上简单路径上的点权之和,你会发现它等于我们要求的东西!

现在,由于我们要计算所有点对,转化为计算各点的点权对答案的贡献,即:计算有多少圆点之间路径经过了这个点,这个就和上一个例题挺像了,在这个题的代码实现中我们在 DFS 上直接处理这个东西。

于是做完了!

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;
#define int long long
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=3e5+5;
struct edge{
  int u,v,nxt;
}e[N*2];
int _head[N];int idx=-1;
void add(int u,int v){
  idx++;
  e[idx].u=u;e[idx].v=v;e[idx].nxt=_head[u];
  _head[u]=idx;
  return ;
}
int dfn[N],low[N],id,siz[N];
stack<int> s;
vector<int> col[N];
int vdcc=0;
void tarjan(int u,int fa){
  dfn[u]=low[u]=++id;
  s.push(u);
  for(int i=_head[u];i!=-1;i=e[i].nxt){
    int v=e[i].v;
    if(fa==(i^1))continue;
    if(!dfn[v]){
      tarjan(v,i);
      low[u]=min(low[u],low[v]);
      if(dfn[u]<=low[v]){
        vdcc++;
        col[vdcc].push_back(u);
        siz[vdcc]++;
        while(s.top()!=v){
          int vi=s.top();s.pop();
          col[vdcc].push_back(vi);
          siz[vdcc]++;
        }
        s.pop();col[vdcc].push_back(v);
        siz[vdcc]++;
      }
    }
    else low[u]=min(low[u],dfn[v]);
  }
}
vector<int> ed[N];
int nw[N],sz[N];
ll ans=0;
int n,m;
int c[N],szm[N];
bool vis[N];
void dfs(int u,int fa,int cc){
  bool inner=0;
  c[u]=cc;
  for(int v:ed[u]){
    if(v==fa)continue;
    inner=1;
    dfs(v,u,cc);
    sz[u]+=sz[v];
  }
  if(u<=n)sz[u]++;
  return ;
}
void dfs1(int u,int fa,int tot){
  if(u<=n)sz[u]++;
  vis[u]=1;
  for(int v:ed[u]){
    if(v==fa)continue;
    dfs1(v,u,tot);
    ans+=sz[v]*sz[u]*2*nw[u];
    sz[u]+=sz[v];
  }
  ans+=(tot-sz[u])*sz[u]*2*nw[u];
  return ;
}
signed main(){
  
  memset(_head,-1,sizeof _head);
  cin>>n>>m;vdcc=n;
  for(int i=1;i<=m;i++){
    int u,v;
    u=rd;v=rd;
    add(u,v);add(v,u);
  }
  for(int i=1;i<=n;i++)if(!dfn[i])tarjan(i,-1);
  for(int i=n+1;i<=vdcc;i++){
    for(int v:col[i]){ed[i].push_back(v);ed[v].push_back(i);}
  }
  for(int i=1;i<=vdcc;i++){
    if(i<=n)nw[i]=-1;
    else nw[i]=siz[i];
  }
  int cnt=0;
  for(int i=n+1;i<=vdcc;i++){
    if(!sz[i]){dfs(i,-1,++cnt);szm[cnt]=sz[i];}
  }
  memset(sz,0,sizeof sz);
  for(int i=n+1;i<=vdcc;i++){
    if(!vis[i]){dfs1(i,-1,szm[c[i]]);}
  }
  cout<<ans;
    
  return 0;
}
posted @ 2025-06-04 21:24  hm2ns  阅读(31)  评论(0)    收藏  举报