洛谷P3916
题目描述
给出N个点,M条边的有向图,对于每个点v,求A(v)表示从点v出发,能到达的编号最大的点。
输入格式:
第1 行,2 个整数N,M。
接下来M行,每行2个整数U_i,V_i ,表示边(U_i,V_i)。点用1,2,⋯,N编号。
输出格式:
N 个整数A(1),A(2),⋯,A(N)。
输入输出样例
输入样例#1:
4 3
1 2
2 4
4 3
输出样例#1:
4 4 3 4
说明
• 对于60% 的数据1≤N、K≤10^3;
• 对于100% 的数据,1≤N,M≤10^5。
分析
这个题有很多种解法,比如说tarjin;bfs;dfs;dp;……慢慢等你来探索吧!
解法一:60分的暴力枚举+队列优化
主要就是对每个点进行枚举,然后从个点开始,枚举他能通过的所有的边,并把没走过的点存入队列中,队列中所有的点再次重复上述过程,就可以保证,这个点能走到的所有的点都在vis数组中有记录,最后看vis数组中标记的最大的点就可以。
#include<iostream>
#include<cstring>
#include<algorithm>
#include<cstdio>
#include<cmath>
#include<queue>
using namespace std;
queue<int> q;
int tot,to[200001],fi[200001],ne[200001],n,m,n1,n2;
int vis[100001];
int add(int x,int y)//建立一条由x指向y的边
{
to[++tot]=y;
ne[tot]=fi[x];
fi[x]=tot;
return 0;
}
int main()
{
cin>>n>>m;
for(int i=0;i<m;i++)
{
scanf("%d%d",&n1,&n2);
add(n1,n2);
}
for(int i=1;i<=n;i++)//枚举每一个点,对于第i个点来说,从他开始,看能到达的所有的点
{
memset(vis,0,sizeof(vis));//vis数组表示这个店是否被遍历到
vis[i]++;//从i他自己开始,把他放入队列中,并打上标记
q.push(i);
while(!q.empty())
{
int x=q.front();//取出队首元素,出队
q.pop();
for(int j=fi[x];j;j=ne[j])//从这个点的第一条边开始,把每一条边能到达的点(如果还没被访问过)就放入队列中
{
int y=to[j];
if(vis[y]==0)
{
vis[y]++;
q.push(y);
}
}
}
for(int j=n;j;j--)//倒序寻找最大的能被访问到的点。
{
if(vis[j]){
printf("%d ",j);
j=1;
}
}
}
return 0;
}
解法二:100分做法一,100遍dp
这个解法比较玄学,并且有卡出题人的数据的意思,容易被出题人卡掉。主要的dp思想是:对于一条x→y边,所造成的影响是更新了x所能到达的最远的点,那么就从最后一条边开始,依次更新所能到达的最远的点;这里还有一个环的情况,主要使用多次重复这个dp的过程,也就是重复了100遍dp。还有一个预处理环节,按照每一条有向边指向的点的大小进行排序。
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
using namespace std;
int n,m,dp[100001];
struct aaa{
int x;int y;
}a[100001];
int cmp(aaa n1,aaa n2)
{
return n1.y>n2.y;
}
int main()
{
cin>>n>>m;
for(int i=0;i<=n;i++)
dp[i]=i;
for(int i=0;i<m;i++)
scanf("%d%d",&a[i].x,&a[i].y);
sort(a,a+m,cmp);
for(int t=0;t<100;t++)
for(int j=0;j<m;j++)
dp[a[j].x]=max(dp[a[j].x],dp[a[j].y]);
for(int i=1;i<=n;i++)
printf("%d ",dp[i]);
return 0;
}
解法三:100分做法二,反向dfs法
主要的思路就是从最后一个点开始,看看这个点所能到达的前面的所有的点。把本来应该x→y的边改为y→x,也就是由一个点所能到达的最远的点改为一个点所能到达的所有的点,好处是之前的算法时间复杂度是O(N*M)的;而由一个点所能到的其他的点的时候,只要这个点已经被找到了,就不需要再进行判断了,时间复杂度就降为了O(N+M)。
#include<iostream>
#include<cstring>
#include<algorithm>
#include<cstdio>
#include<cmath>
using namespace std;
int tot,to[200001],fi[200001],ne[200001],n,m,n1,n2;
int ans[100001];
int add(int x,int y)//建立一条由x指向y的边
{
to[++tot]=y;
ne[tot]=fi[x];
fi[x]=tot;
return 0;
}
int dfs(int x,int y)//x子节点,y最终点
{
if(ans[x])return 0;
ans[x]=y;
for(int i=fi[x];i;i=ne[i])
if(ans[to[i]]==0)
dfs(to[i],y);
return 0;
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=0;i<m;i++)
{
scanf("%d%d",&n1,&n2);
add(n2,n1);//反向加边
}
for(int i=n;i>0;i--)
dfs(i,i);
for(int i=1;i<=n;i++)
printf("%d ",ans[i]);
return 0;
}
解法四:100分做法三,反向bfs+队列优化
与反向dfs相比,这两种做法差不多,基本思路和时间复杂度差不多,每个点和边都只会查询一次,时间复杂度都是O(N+M);但是使用bfs+队列可以防止调用dfs次数太多引起爆栈。(虽然NOIP中可以增加调用栈的空间,略感麻烦)
代码中有解释
#include<iostream>
#include<cstring>
#include<algorithm>
#include<cstdio>
#include<cmath>
#include<queue>
using namespace std;
queue<int> q;
int tot,to[200001],fi[200001],ne[200001],n,m,n1,n2;
int ans[100001];
int add(int x,int y)//建立一条由x指向y的边
{
to[++tot]=y;
ne[tot]=fi[x];
fi[x]=tot;
return 0;
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=0;i<m;i++)
{
scanf("%d%d",&n1,&n2);
add(n2,n1);//反向加边
}
for(int i=n;i>0;i--)
{
if(!ans[i])//对于每一个终点i,看有多少点的终点是他
{
ans[i]=i;
q.push(i);//i入队
while(!q.empty())//只要队列中还有元素
{
int x=q.front();//取对首元素,将所有到这个点的点入队
q.pop();
for(int j=fi[x];j;j=ne[j])
if(!ans[to[j]])
{
q.push(to[j]);
ans[to[j]]=i;
}
}
}
}
for(int i=1;i<=n;i++)
printf("%d ",ans[i]);
return 0;
}
未完待续。。。。。。
基本上这四种思路是比较简单的了,像tarjin这种大佬才会的思路可以暂时放一放。