图论一般题合集
title: 图论一般题合集
categories:
- ICPC
tags:
- null
abbrlink: ed979e34
date: 2024-05-26 00:00:00
病毒溯源
题意:给定一棵树,求出最大深度,要求输出字典序最小的路径
Solution:由于看错题,以为是dag,然后发现只需要拓扑排序一下然后dp最长路就可以了。但值得注意的是需要提前对邻接表排序保证字典序,每次更新都需要维护终点,必须在过程中维护。我们找的是后缀最大值,但希望前缀结构最小。
vector<int>e[N];
int din[N];
vector<int>tp;
vector<int>dp(N+1,0);
void topsort(){
queue<int>q;
for(int i=0;i<=n-1;i++)if(din[i]==0){q.push(i);dp[i]=1;}
while(q.size()){
auto u=q.front();q.pop();
tp.push_back(u);
for(auto v:e[u]){
din[v]--;
if(din[v]==0)q.push(v);
}
}
}
//没仔细读题,以为是dag,结果是树
void solve(){
cin>>n;
for(int i=0;i<=n-1;i++){
int num;cin>>num;
for(int j=1;j<=num;j++){
int x;cin>>x;
din[x]++;
e[i].push_back(x);
}
}
for(int i=1;i<=n;i++)sort(e[i-1].begin(),e[i-1].end());
topsort();
//vector<int>dp(n+1,0);
//提前维护终点
int pos=0,len=0;
vector<int>pre(n+1,-1);
for(auto u:tp){
//cerr<<u<<endl;
for(auto v:e[u]){
if(dp[v]<dp[u]+1){
dp[v]=dp[u]+1;
pre[v]=u;
if(dp[v]>len){
len=dp[v];
pos=v;
}
}
// else if(dp[v]==dp[u]+1){
// if(pre[v]>u){
// pre[v]=u;
// }
// }
}
}
// cerr<<len<<endl;
//找终点错误,这样无法保证答案字典序最小
vector<int>ans;
for(int i=pos;i!=-1;i=pre[i])ans.push_back(i);
cout<<ans.size()<<endl;
for(int i=ans.size()-1;i>0;i--)cout<<ans[i]<<" ";
cout<<ans[0];
}
Solution:每当发现需要拓扑序dp的时候就应该想到记忆化搜索,如果发现是树可能想到的比较快。
vector<int>e[N];
int son[N];
bool st[N];
int dfs(int u){
//cerr<<u<<endl;
int res=0;
for(auto v:e[u]){
int rr=dfs(v);
if(res<rr){
res=rr;son[u]=v;
}
else if(res==rr){
son[u]=min(son[u],v);
}
}
//cerr<<u<<" "<<res<<endl;
return res+1;
}
void solve(){
cin>>n;
for(int i=0;i<=n-1;i++){
int num;cin>>num;
for(int j=1;j<=num;j++){
int x;cin>>x;
e[i].push_back(x);
st[x]=true;
}
}
memset(son,-1,sizeof son);
int root=0;
while(st[root])root++;
int ans=dfs(root);
cout<<ans<<endl;
cout<<root;
for(int i=son[root];i!=-1;i=son[i])cout<<" "<<i;
}
送外卖https://www.acwing.com/problem/content/description/4477/
题意:给定一棵树,每次给点集加一个点,求从根出发到达当前点集所有点至少一次的最短总路程
Solution:对于这种每次都与上一次问题有细微变化的题目,我们考虑微观分析。然而我们发现依旧难以下手,因为我们的终点不确定,就完全没有确定的推理根基。。考虑对称性:加强题目条件,要求每次都要回到根。
按照加强题目的条件来考虑,对于当前点集的所有的点,我们最多走向他一次,离开一次,显然这样贪心是最优的,所有相关边最多会走两次,回到原问题我们需要钦定一个终点使得答案尽可能减少,选择深度最大的点即可。
实现方面:我们可以预处理所有点的深度,方便更新钦定终点,每次我们只需要对于新加进来的点考虑需要哪些新边,这些边只会走两次,以后就不用统计他们了,所以需要记忆化搜索,每次让新点向根跳,直到跳到已经统计过的边,考虑整个过程不难想到上面的边也已经统计过了。
int fa[N];
bool st[N];
int dep[N];
int mx=0,sum=0;
vector<int>e[N];
void dfs1(int u,int fa){
dep[u]=dep[fa]+1;
for(auto v:e[u]){
dfs1(v,u);
}
}
int root;
void dfs2(int u){
if(u==root||st[u])return ;
sum++;
dfs2(fa[u]);
st[u]=true;
}
void solve(){
cin>>n>>m;
//int root=0;
for(int i=1;i<=n;i++){
int x;cin>>x;
fa[i]=x;
if(x!=-1)e[x].push_back(i);
if(x==-1)root=i;
}
dfs1(root,0);
for(int i=1;i<=m;i++){
int x;cin>>x;
mx=max(dep[x],mx);
//cerr<<x<<" "<<dep[x]<<endl;
dfs2(x);
int ans=2*sum-mx+1;
cout<<ans<<endl;
}
}
千手观音https://www.acwing.com/activity/content/code/content/3586865/
题意:每个字符串代表一个数,连起来能够代表进制数,按大小顺序给出一些字符串组合起来的进制数,现在让你给这些字符串排序,如果存在不确定关系,字典序小的在前面。
Solution:首先明确这个关系具有传递性,可以证明,于是我们只需要相邻比较去建边.其次考虑两个位数不同的进制数是无法比较的(eg. 189和145.999)。我们只需要把能比较的第一位不同的建立边。用优先队列完成拓扑排序去满足字典序最小。
实现:我们需要哈希表去将字符串映射,用get函数将一个进制数的几个字符串全部拆成vector,便于我们处理,剩下的操作全都是基本图论。
debug:为了保证所有涉及到的字符串都会参与拓扑排序,我们需要在哈希表种创建他们。但是只创建一次,第二次再赋值为0会覆盖之前的入度信息。
unordered_map<string,int>din;
unordered_map<string,vector<string>>e;
vector<string>get(string s){
vector<string>res;
string word;
for(auto x:s){
if(x=='.'){
res.push_back(word); if(din.count(word)==0)din[word]=0;
word="";
}
else word+=x;
}
//必须加判断条件,不然会丢失之前的入度信息
if(din.count(word)==0)din[word]=0;//意义是所有点都能出现在未来topsort
res.push_back(word);
return res;
}
vector<string>tp;
void topsort(){
//cerr<<endl;
priority_queue<string,vector<string>,greater<string>>q;
for(auto [x,y]:din)if(y==0)q.push(x);
while(q.size()){
auto u=q.top();q.pop();tp.push_back(u);
for(auto v:e[u]){
//cerr<<u<<" "<<v<<endl;
din[v]--;
if(din[v]==0)q.push(v);
}
}
}
void solve(){
cin>>n;
string str;
cin>>str;
auto last=get(str);
for(int i=1;i<=n-1;i++){
string s;cin>>s;
auto cur=get(s);//当位数不同的时候无法进行比较。这里的.只是分割符,
//不是小数点
if(cur.size()==last.size()){
for(int j=0;j<cur.size();j++){
if(cur[j]!=last[j]){
e[last[j]].push_back(cur[j]);
// cerr<<din["cn"]<<endl;
//cerr<<i<<" "<<last[j]<<" "<<cur[j]<<endl;
din[cur[j]]+=1;
//cerr<<din["cn"]<<endl;
break;
}
}
}
last=cur;
}
//cerr<<din["cn"]<<endl;
topsort();
cout<<tp[0];
for(int i=1;i<tp.size();i++)cout<<"."<<tp[i];
}
给一颗基环树,请你输出所有环上的点。
Soluton1.考虑拓扑排序的思想,由于是无向图所以每个点至少出度为1,环上的点至少度为2,所以就只是偏移了入队标准,其他和拓扑排序一摸一样
vector<int>e[N];
int din[N];
void ts(){
queue<int>q;
for(int i=1;i<=n;i++)if(din[i]==1)q.push(i);
while(q.size()){
auto u=q.front();
q.pop();
for(auto v:e[u]){
din[v]--;
if(din[v]==1)q.push(v);
}
}
}
void solve(){
cin>>n;
DSU dsu(n+1);
for(int i=1;i<=n;i++){
int u,v;cin>>u>>v;
e[u].push_back(v);
e[v].push_back(u);
din[v]++;din[u]++;
}
ts();
for(int i=1;i<=n;i++)if(din[i]>1)cout<<i<<" ";
}
Solution2.考虑并查集找环,第一次合并失败的时候,必然这两个点都在环上。不加这个边,原图变成树,两个点之间的路径唯一,直接维护父节点一路倒退回去,或者对节点标记,再扫一遍。这里用set是偷懒了
vector<int>e[N];
int fa[N];
void solve(){
cin>>n;
DSU dsu(n+1);
int l,r;
for(int i=1;i<=n;i++){
int u,v;cin>>u>>v;
if(!dsu.merge(u,v)){l=u,r=v;continue;}
e[u].push_back(v);
e[v].push_back(u);
}
auto dfs=[&](auto self,int u,int f)->void{
fa[u]=f;
for(auto v:e[u]){
if(v==f)continue;
self(self,v,u);
}
};
dfs(dfs,r,0);
set<int>s;
for(int j=l;j;j=fa[j])s.insert(j);
for(auto x:s)cout<<x<<" ";
}
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 爱飞鱼的blog!