最小生成树思想
我们的标题叫做思想,这也就意味着这些题目并不是单纯的要求最小生成树。
我们知道,求最小生成树的方法有几个来?prim 和 kruskal 两种对吧。
我们来回顾一下 prim 和 kruskal 两种算法的本质。
我们知道,两种算法都是贪心,但是对于 kruskal 而言,他的本质更趋于集合合并,所以针对这个点出题人经常喜欢考 dsu on tree 等与集合相关的题。
我们来看几个典例。
CF888G
作为场上最后一道题,他的通过数却比 CF888F 多,可见并不是所有的 G 都不可做。
我们先来想一想怎么做,看到 \(a_i\oplus a_j\) 一共就想这么几个常见的,第一线性基,第二 01 Trie,第三二进制拆分。
首先这里用线性基实在是不合理,因为他不是求最大的异或和,显然我们这道题二进制拆分也不是很好做(或许有二进制拆分后暴力建边的?不管了)。
所以我们想到建立字典树。
我们把所有的 \(a_i\) 装入一个字典树,然后我们考虑怎么求最小生成树。
我们还是朴素的回溯到最小生成树的思路上,即选取最小的边权,判断是否合法,然后加入。
那么在字典树上怎么找到最小的边权呢?
我们发现,每一次最小的边权就是能合并的两个数在字典树上 LCA 最大的。所以我们考虑遍历字典树,从下往上合并子树,显然合并之前子树之间不连通,所以也不需要并查集维护了。我们采用树上启发式合并。
至于找到两个子树之间的最小边权,我们遍历集合大小较小的权值,然后我们去 \(\mathcal O(\log V)\) 的求最小值即可,不会的看这里。
总复杂度为 \(\mathcal O(n\log n\log V)\),可以通过本题。
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=2e5+5;
int T[N*20][2],cnt,ans,a[N*20],dep[N*20];
vector<int> G[N*20];
void ins(int x)
{
int cur=0;
for(int i=30;~i;i--)
{
int c=(x>>i)&1;
if(!T[cur][c])
T[cur][c]=++cnt,a[cnt]=c,dep[cnt]=i;
cur=T[cur][c];
}
G[cur].push_back(x);
}
int DFS(int y,int v)
{
int cur=y,res=1<<dep[y];
for(int i=dep[y]-1;~i;i--)
{
int c=(v>>i)&1;
if(T[cur][c]) cur=T[cur][c];
else
{
res|=(1<<i);
cur=T[cur][c^1];
}
}
return res;
}
void dfs(int x)
{
bool flag=false;
if(T[x][0]) dfs(T[x][0]),flag=true;
if(T[x][1]) dfs(T[x][1]),flag=true;
if(!flag) return;
if(!T[x][0])
{
swap(G[x],G[T[x][1]]);
return;
}
if(!T[x][1])
{
swap(G[x],G[T[x][0]]);
return;
}
if(x==27)
{
new char;
}
int s1=T[x][0],s2=T[x][1];
if(G[s1].size()>G[s2].size()) swap(s1,s2);
int mn=1e9;
for(int u:G[s1])
{
G[s2].push_back(u);
mn=min(mn,DFS(s2,u));
}
swap(G[x],G[s2]);
ans+=mn;
}
signed main()
{
int n;cin>>n;
for(int i=1,x;i<=n;i++)
cin>>x,ins(x);
dfs(0);
cout<<ans;
return 0;
}
P3639 [APIO2013] 道路费用
这道题大家只需看懂就行,别去写,小心写到吐/tuu。
首先想个暴力的做法,我们看到 \(k\) 这么小就可以直接状压了。
我们枚举这 \(k\) 条边的选或不选的情况,然后我们再去做最小生成树即可,复杂度为 \(\mathcal O(2^km\log m)\),铁定 TLE。
怎么办呢?我们在枚举的时候,应该注意 \(0\) 与 \(2^k-1\) 时,我们所选的其他的边的情况。我们容易发现,当我们全选 \(k\) 条边时,假设其他的边集为 \(R\),那么当我们的状态 \(S\in [0,2^k-1]\) 时,\(R\) 总是出现在最小生成树里。
证明感性理解就行了,不需要会证……
那么这样我们的复杂度就大大降低了,我们对于 \(R\) 进行缩点,那么最后就有 \(k+1\) 个连通块,那么我们这个时候再去 \(2^k\) 枚举时,我们的边数就变成了 \(2k\) 条边,所以复杂度即为 \(\mathcal O(2^kk\log k)\),通过本题。
听着简单,实际非常难写,首先你要跑生成树,然后你要缩点,然后还要跑状压,状压的时候想要计算权值还需要 LCA…………
代码千万行,安全第一行。不是大肝帝,不要写此题!
#include<bits/stdc++.h>
#define int long long
#define pii pair<int,int>
#define f first
#define s second
using namespace std;
const int N=1e5+5,M=3e5+5,K=55;
struct node
{
int u,v,w;
friend bool operator<(const node &x,const node &y)
{
return x.w<y.w;
}
}E1[M],E2[M];
pii e[K];
int n,m,k,a[N],scc[N],cnt,val[K],rt,tot,vis[K<<1],fa[K],dep[K],V[K],mx[K],sum,ans,ne[K],to[K],h[K],num;
void add(int u,int v)
{
ne[num]=h[u];
to[num]=v;
h[u]=num++;
}
struct DSU
{
int f[N];
int find(int x){return x==f[x]?x:f[x]=find(f[x]);}
}D1,D2,D3;
void dfs(int x,int F)
{
fa[x]=F,dep[x]=dep[F]+1,V[x]=val[x];
for(int i=h[x];~i;i=ne[i])
{
int v=to[i];
if(v==F) continue;
dfs(v,x),V[x]+=V[v];
}
}
void ST(int u,int v,int w)
{
if(dep[u]<dep[v]) swap(u,v);
while(dep[u]>dep[v])
{
mx[u]=min(mx[u],w);
u=fa[u];
}
while(u!=v)
{
mx[u]=min(mx[u],w);
mx[v]=min(mx[v],w);
u=fa[u],v=fa[v];
}
}
signed main()
{
cin>>n>>m>>k;
for(int i=1;i<=m;i++)
cin>>E1[i].u>>E1[i].v>>E1[i].w;
for(int i=1;i<=k;i++)
cin>>e[i].f>>e[i].s;
sort(E1+1,E1+1+m);
for(int i=1;i<=n;i++)
cin>>a[i];
for(int i=1;i<=n;i++)
D1.f[i]=D2.f[i]=i;
for(int i=1;i<=k;i++)
{
int u=e[i].f,v=e[i].s;
int fu=D1.find(u),fv=D1.find(v);
if(fu!=fv) D1.f[fu]=D1.f[fv];
}
for(int i=1;i<=m;i++)
{
int u=E1[i].u,v=E1[i].v;
int fu=D1.find(u),fv=D1.find(v);
if(fu==fv) continue;
D1.f[fu]=D1.f[fv];
fu=D2.find(u),fv=D2.find(v);
if(fu!=fv) D2.f[fu]=D2.f[fv];
}
for(int i=1;i<=n;i++)
if(D2.find(i)==i)
{
scc[i]=++cnt;
val[cnt]=a[i];
}
for(int i=1;i<=n;i++)
if(D2.find(i)!=i)
{
scc[i]=scc[D2.find(i)];
val[scc[i]]+=a[i];
}
rt=scc[1];
for(int i=1;i<=m;i++)
{
int u=E1[i].u,v=E1[i].v;
int fu=D2.find(u),fv=D2.find(v);
if(fu==fv) continue;
E2[++tot]={scc[u],scc[v],E1[i].w};
D2.f[fu]=D2.f[fv];
}
for(int S=0;S<(1<<k);S++)
{
num=0;
for(int i=1;i<=cnt;i++) D3.f[i]=i;
memset(h,-1,sizeof(h));
memset(vis,0,sizeof(vis));
memset(mx,0x3f,sizeof(mx));
memset(fa,0,sizeof(fa));
memset(dep,0,sizeof(dep));
memset(V,0,sizeof(V));
for(int i=1;i<=k;i++)
{
if((S>>(i-1))&1^1) continue;
int u=scc[e[i].f],v=scc[e[i].s];
int fu=D3.find(u),fv=D3.find(v);
if(fu==fv) continue;
D3.f[fu]=D3.f[fv],add(u,v),add(v,u);
}
for(int i=1;i<=tot;i++)
{
int u=E2[i].u,v=E2[i].v;
int fu=D3.find(u),fv=D3.find(v);
if(fu==fv) continue;
D3.f[fu]=D3.f[fv];
vis[i]=1,add(u,v),add(v,u);
}
dfs(rt,0);
for(int i=1;i<=tot;i++)
{
if(vis[i]) continue;
ST(E2[i].u,E2[i].v,E2[i].w);
}
sum=0;
for(int i=1;i<=k;i++)
{
if((S>>(i-1))&1^1) continue;
int u=scc[e[i].f],v=scc[e[i].s];
if(dep[u]<dep[v]) swap(u,v);
sum+=mx[u]*V[u];
}
ans=max(ans,sum);
}
printf("%lld",ans);
return 0;
}
P4784 [BalticOI 2016] 城市 (Day2)
这道题其实就是模板最小斯坦纳树。这道题还是很好写的,而且是双倍经验。
对于最小斯坦纳树来说,我们不跑 kruskal,而是进行 dp。
我们设 \(f_{i,S}\) 表示枚举到第 \(i\) 个城市,然后我们所包含的关键城市的状态为 \(S\),所需要的最小总成本。
首先是初始化,这很简单。\(f_{a_i,2^{i-1}}=0\)。
其次考虑转移。
- 如果是不同状态的转移,我们发现 \(f_{i,S}=\min_{T\in S}(f_{i,S},f_{i,T}+f_{i,S-T})\)。这个复杂度是 \(\mathcal O(n3^k)\)。
- 如果是同一状态的转移,我们发现 \(f_{i,S}=\min_{(i,j)\in E}(f_{i,S},f_{j,S}+w)\),其中 \(w\) 为 \((i,j)\) 的边权,对于这种情况我们可以用
DIJ松弛一遍,复杂度为 \(\mathcal O(2^km\log m)\)。
综上,总时间复杂度为 \(\mathcal O(3^kn+2^km\log m)\),可以通过。这就是模板斯坦纳树。
#include<bits/stdc++.h>
#define fi first
#define se second
using namespace std;
using ll=long long;
using pil=pair<int,ll>;
using pli=pair<ll,int>;
const ll INF=0x3f3f3f3f3f3f3f3f;
const int N=2e5+5;
int n,m,k,vis[N];
ll f[N][40];
vector<pil> G[N];
priority_queue<pli> q;
void dij(int S)
{
memset(vis,0,sizeof(vis));
while(!q.empty())
{
pli tmp=q.top();q.pop();
int x=tmp.se;
if(vis[x]) continue;
vis[x]=1;
for(pil tmp:G[x])
{
int v=tmp.fi;ll w=tmp.se;
if(f[v][S]>f[x][S]+w)
{
f[v][S]=f[x][S]+w;
q.push({-f[v][S],v});
}
}
}
}
int main()
{
cin>>n>>k>>m;
memset(f,0x3f,sizeof(f));
for(int i=1,x;i<=k;i++)
cin>>x,f[x][1<<(i-1)]=0;
for(int i=1;i<=m;i++)
{
int u,v,w;cin>>u>>v>>w;
G[u].emplace_back(v,w);
G[v].emplace_back(u,w);
}
for(int S=0;S<(1<<k);S++)
{
for(int i=1;i<=n;i++)
{
for(int T=S&(S-1);T;T=S&(T-1))
f[i][S]=min(f[i][S],f[i][T]+f[i][S^T]);
if(f[i][S]<INF) q.push({-f[i][S],i});
}
dij(S);
}
ll mn=1e18;
for(int i=1;i<=n;i++)
mn=min(mn,f[i][(1<<k)-1]);
cout<<mn;
return 0;
}

浙公网安备 33010602011771号