【好题选讲】图论阴间题选讲 III (图的连通性与缩点)
P2515 [HAOI2010] 软件安装
题意
给定 \(N\) 个软件,每个软件有:
- 空间消耗 \(W_i\)
- 价值 \(V_i\)
- 依赖关系 \(D_i\)(\(D_i=0\) 表示无依赖,每个软件仅有一个依赖)
选择若干软件安装到容量为 \(M\) 的磁盘中,满足:
- 若安装软件 \(i\),则必须安装其依赖链上的所有软件
- 最大化总价值 \(\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;
}

浙公网安备 33010602011771号