您的位置:首页 > 其它

2017.4.3 机房测试 (并查集,最短路,拓扑排序,最小生成树)

2017-04-03 21:53 701 查看
jyb远在北京仍不忘为我们出题。

这次是喜闻乐见的图论专题。



【解题报告】

用链表什么的暴力枚举题意,最坏复杂度是O(m^2)的。

注意到每次都是移动一摞砖,其实问题就是集合的合并,只是这个集合是一个有序的,我们还需要知道每个元素在集合中的位置。我们可以利用并查集高效的来解决。我们引入一个dep数组,对于祖先节点(将每摞砖最上方的一块设为祖先,设最下面的为祖先也是可以的),我们记录该集合中一共有多少块砖,对于其他节点,我们记录它上方有多少块砖,这样我们可以很容易地计算出下方有多少块。这样在合并x和y(设他们祖先为fx,fy)时,我们将dep[fx]赋给dep[fy](因为移到了上方,fx有多少块fy上方就有多少块),然后dep[fx]要加上原本的dep[fy]。难点就是在合并的过程中,dep数组怎么维护。其实很简单,只需要在路径压缩时顺带维护即可,如果一个节点x的祖先fx不是祖先(即已被合并过),那么dep[x]加上dep[fx]即可(等于头上又多了一摞砖,所以要加上)。

时间复杂度O(α(m))

代码如下:

#include<cstdlib>
#include<cstring>
#include<cmath>
#include<algorithm>
#include<string>
#include<iostream>
#include<queue>
#include<cstdio>

using namespace std;

int n,m;
int x,y;
char c;
int father,t1,t2;
int fa[3000010];        //
int dep[3000010];       //

int getfather(int x)
{
if(x == fa[x])      //
return x;
else
{
int f = fa[x];              //用来判断自己记录的祖先是否真的是祖先
fa[x] = getfather(fa[x]);   //路径压缩
if(fa[f] != f)              //如果不是,则需要更新上面的砖块数
dep[x] += dep[f];
return fa[x];
}
}

void Init()                         //
{
for(int i = 1; i <= n; i++)
{
fa[i] = i;
dep[i] = 1;
}
}

int main()
{
freopen("bricks.in","r",stdin);
freopen("bricks.out","w",stdout);
scanf("%d%d",&n,&m);
Init();
while(m--)
{
scanf(" %s",&c);
if(c == 'M')
{
scanf("%d%d",&x,&y);
t1 = getfather(x);
t2 = getfather(y);
if(t1 != t2)
{
fa[t2] = t1;            //合并
int ll = dep[t2];       //t2上面的砖块数等于t1那摞砖的总数
dep[t2] = dep[t1];      //
dep[t1] += ll;          //而t1的总数也需要加上t2的总数
}
}
else
{
scanf("%d",&x);             //因为问的在下面的砖块,所以减去自己
if(x == fa[x])              //因为祖先和非祖先,dep数组的含义不                                            //同需要分开讨论,祖先减去自己即可
{
printf("%d\n",dep[x] - 1);
continue;
}
father = getfather(x);      //非祖先的话,因为dep保存的是自己上面还有多少个,所以用总的减去上面的和自己,即可得到下面有多少个
printf("%d\n",dep[father] - dep[x] - 1);
}
}
}


下面是自己的代码

//poj 1998
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
#define N 100000

int n,m;
int fa[2*N],sum[2*N],dis[2*N];

int getfather(int x)
{
int t;
if(fa[x]!=x)
{
t=fa[x];
fa[x]=getfather(fa[x]);
dis[x]+=dis[t];
}
return fa[x];
}
void move(int x,int y)
{
x=getfather(x),y=getfather(y);
if(x!=y)
{
fa[y]=x;
dis[y]=sum[x];
sum[x]+=sum[y];
}
}
int main()
{
freopen("bricks.in","r",stdin);
freopen("bricks.out","w",stdout);
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
fa[i]=i,sum[i]=1,dis[i]=0;
while(m--)
{
char s[10];
int x,y;
scanf("%s",s);
if(s[0]=='M')
{
scanf("%d%d",&x,&y);
move(x,y);
}
else
{
scanf("%d",&x);
printf("%d\n",sum[getfather(x)]-dis[x]-1);
}
}
return 0;
}




【解题报告】

如果没有大雾天气,很显然就是求一个最短路就行了。但是由于大雾天气的存在,如果大雾天气出现在乐最短路的路径上,那么肯定不能按照最快的时间到达,如果这样的话很可能就失信与客户。所以我们就需要枚举最短路径上每条高速公路出现大雾天气的情况,再求最短路,可得一个最糟的情况,即得到在不失信于客户的前提下的最快时间。

注意到数据范围,每条高速公路的行驶时间最多相差10倍,spfa的时间复杂度的上界大概为O(kM),k = 10,而朴素Dijkstra求一次最短路为稳定O(n^2),总的最坏为O(n^3),所以我们采用spfa或者堆优化Dijkstra算法(jyb写的STL堆优被卡了两个点)求最短路。

(特别坑的是题里面是单向边)

代码如下:

#include <cstdio>
#include <cmath>
#include <cstring>
#include <string>
#include <cstdlib>
#include <algorithm>
#include <iostream>

using namespace std;

const int maxn = 4010;
const int maxm = 20010;

struct edge
{
int u,v,next;
double w;
}e[maxm];
int h[maxn],num;
double dis[maxn];
int pre[maxn];
bool vis[maxn];
int q[maxn],head,tail;
int n,m;

void build(int u,int v,double w)
{
num++;
e[num].u = u;
e[num].v = v;
e[num].w = w;
e[num].next = h[u];
h[u] = num;
}

void init()
{
for(int i = 2; i <= n; i++)
dis[i] = 1000000.0;
head = 0;
tail = 1;
q[0] = 1;
vis[1] = true;
}

double spfa(bool first)
{
int v,u;
init();
while(head != tail)
{
u = q[head];
for(int i = h[u]; i; i = e[i].next)
{
v = e[i].v;
if(dis[v] > dis[u] + e[i].w)
{
dis[v] = dis[u] + e[i].w;
if(first)
pre[v] = i;
if(!vis[v])
{
vis[v] = true;
q[tail] = v;
tail = (tail + 1) % n;
}
}
}
vis[u] = false;
head = (head + 1) % n;
}
return dis
;
}

int main()
{
freopen("drive.in","r",stdin);
freopen("drive.out","w",stdout);
scanf("%d%d",&n,&m);
int u,v;
double sp,len;
for(int i = 1; i <= m; i++)
{
scanf("%d%d%lf%lf",&u,&v,&sp,&len);
build(u,v,len/sp);
}
int now = n;
double ans = spfa(true);
while(now != 1)
{
e[pre[now]].w *= 4.0;
ans = max(ans,spfa(false));
e[pre[now]].w /= 4.0;
now = e[pre[now]].u;
}
printf("%.4lf\n",ans);
return 0;
}




暴力做法:Floyd求任意两点连通性。(给了30%的分)

我们知道,对于在同一SCC中的两点,它们是互相可达的,这也意味着一个SCC中的所有点和其他点的连通性都是一样的,显然我们可以将原图缩点简化问题。经过tarjan缩点后,得到了一个DAG,我们很容易想到如果一个图“分叉”,那么分叉上的点肯定是互不可达的,所以原图必须是“一条链”。为什么对链打引号呢?是因为这条链不用很严格,比如1->2 2->3 3->4 1->4 2->4这样的图也是满足的,且可能还有重边,所以只用入度出度去判断的方法容易出错,dfs去走一条链的办法也比较麻烦(我没想出有什么简单的办法)。我们利用拓扑排序,若一直都是一个入度为0的点(队中至多有一个点),那么就是满足题意的。

Ps:可以先写一个floyd来对拍(因为时间代价很低)

时间复杂度O(n+m)

代码如下:

#include <cstdio>
#include <cmath>
#include <cstring>
#include <cstdlib>
#include <iostream>
#include <algorithm>
#include <string>

using namespace std;

const int maxn = 100010;
const int maxm = 400010;

struct edge
{
int v,next;
}e[maxm];
int h[maxn],num;
int indexx;
int top,stack[maxn];
int belong[maxn],cnt;
int hh[maxn],numm;
int q[maxn],head,tail;
int u,v,n,m;
bool vis[maxn];
int p[maxn],dfn[maxn],low[maxn];
int T;

void dfs(int u)                     //tarjan
{
int v;
indexx++;
dfn[u] = indexx;
low[u] = indexx;
vis[u] = true;
stack[++top] = u;
for(int i = h[u]; i != 0; i = e[i].next)
{
v = e[i].v;
if(!dfn[v])
{
dfs(v);
if(low[v] < low[u])
low[u] = low[v];
}
else
if(vis[v] && dfn[v] < low[u])
low[u] = dfn[v];
}
if(dfn[u] == low[u])
{
cnt++;
while(1)
{
int x = stack[top];
vis[x] = false;
belong[x] = cnt;
top--;
if(x == u)
break;
}
}
}

void build(int u,int v)                 //原图建边
{
num++;
e[num].v = v;
e[num].next = h[u];
h[u] = num;
}

void build2(int u,int v)                //缩点后的图建边
{
num++;
e[num].v = v;
e[num].next = hh[u];
hh[u] = num;
}

int main()
{
freopen("graph.in","r",stdin);
freopen("graph.out","w",stdout);
scanf("%d",&T);
while(T--)
{
scanf("%d%d",&n,&m);
num = cnt = indexx = 0;
memset(h,0,sizeof(h));
memset(hh,0,sizeof(hh));
while(m--)
{
scanf("%d%d",&u,&v);
build(u,v);
}
memset(dfn,0,sizeof(dfn));
memset(low,0,sizeof(low));
memset(p,0,sizeof(p));              //记住初始化
for(int i = 1; i <= n; i++)
if(!dfn[i])
dfs(i);
for(int i = 1; i <= n; i++)
{
for(int j = h[i]; j; j = e[j].next)
if(belong[i] != belong[e[j].v])     //不是一个SCC的就建边
{
p[belong[e[j].v]]++;
build2(belong[i],belong[e[j].v]);
}
}
bool ans = true;
head = tail = 0;
for(int i = 1; i <= cnt; i++)           //统计入度为0的
if(!p[i])
q[tail++] = i;
while(head < tail)
{
if(tail - head > 1)                 //队中有超过两个点,就说明不满足条件
{
ans = false;
break;
}
u = q[head];
for(int i = hh[u]; i; i = e[i].next)
{
v = e[i].v;
p[v]--;
if(p[v] == 0)
q[tail++] = v;
}
head++;
}
if(ans)
printf("Yes\n");
else
printf("No\n");
}
return 0;
}


下面是我自己的代码

#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;

const int maxn = 1000010;
const int maxm = 400010;

struct edge
{
int v,next;
}e[maxm];
int h[maxn],num;
int indexx;
int top,stack[maxn];
int belong[maxn],cnt;
int hh[maxn],numm;
int q[maxn],head,tail;
int u,v,n,m;
bool vis[maxn];
int p[maxn],dfn[maxn],low[maxn];
int T;
void dfs(int u)                     //tarjan
{
int v;
indexx++;
dfn[u]=indexx;
low[u]=indexx;
vis[u]=true;
stack[++top] = u;
for(int i=h[u];i!=0;i=e[i].next)
{
v=e[i].v;
if(!dfn[v])
{
dfs(v);
if(low[v] < low[u])
low[u] = low[v];
}
else
if(vis[v] && dfn[v] < low[u])
low[u] = dfn[v];
}
if(dfn[u]==low[u])
{
cnt++;
while(1)
{
int x = stack[top];
vis[x] = false;
belong[x] = cnt;
top--;
if(x == u)
break;
}
}
}
void build(int u,int v)                 //原图建边
{
num++;
e[num].v=v;
e[num].next=h[u];
h[u]=num;
}
void build2(int u,int v)                //缩点后的图建边
{
num++;
e[num].v=v;
e[num].next=hh[u];
hh[u]=num;
}
int main()
{
freopen("graph.in","r",stdin);
freopen("graph.out","w",stdout);
scanf("%d",&T);
while(T--)
{
scanf("%d%d",&n,&m);
num=cnt=indexx=0;
memset(h,0,sizeof(h));
memset(hh,0,sizeof(hh));
while(m--)
{
scanf("%d%d",&u,&v);
build(u,v);
}
memset(dfn,0,sizeof(dfn));
memset(low,0,sizeof(low));
memset(p,0,sizeof(p));              //记住初始化
for(int i=1;i<=n;i++)
if(!dfn[i])
dfs(i);
for(int i=1;i<=n;i++)
for(int j=h[i];j;j=e[j].next)
if(belong[i]!=belong[e[j].v])       //不是一个SCC的就建边
{
p[belong[e[j].v]]++;
build2(belong[i],belong[e[j].v]);
}
bool ans=true;
head=tail=0;
for(int i=1; i<=cnt;i++)            //统计入度为0的
if(!p[i])
q[tail++] = i;
while(head<tail)
{
if(tail-head>1)                 //队中有超过两个点,就说明不满足条件
{
ans=false;
break;
}
u=q[head];
for(int i=hh[u];i;i=e[i].next)
{
v=e[i].v;
p[v]--;
if(p[v]==0)
q[tail++] = v;
}
head++;
}
if(ans)
printf("Yes\n");
else
printf("No\n");
}
return 0;
}




【解题报告】

最朴素的做法:枚举航空公司,将本公司的航线加入,然后做一遍MST,时间复杂度是O(klogk+mk)的。

但我们注意到“在最小生成树中任意一条边都是连接两个集合边权最小的一条边”,这个是MST一个非常重要的结论,是Prim和kruscal算法中贪心的核心。利用这条性质我们可以知道,对于每个航空公司,我们只需要原图MST中得到的航线即可得到最优购买方案。所以更优的做法是:先跑一次MST,把得到的边记录下来,然后枚举航空公司,还是先讲本公司的边全部加进去,然后只用扫一边第一次MST中记录下来的边,用kruscal的方法去构造树即可。时间复杂度为O(klogk+nm)。

对于本题,50%的数据是可以O(mk)过的,最后30%的数据我特别构造了一下,必须枚举到排序后的最后几条边才能构造出一颗生成树,所以O(mk)应该是过不了的,此外的20%数据是随机数据,加了构造出树就跳出判断的代码可能会过(但是jyb写的mk的没过。。);另外,50%的数据需要long long

代码如下:

#include <cstdio>
#include <cmath>
#include <cstring>
#include <string>
#include <cstdlib>
#include <algorithm>
#include <iostream>

using namespace std;
const int maxm = 200002;
const int maxn = 2002;
const long long INF = 1ll << 60;

struct line
{
int u,v;
long long w;
bool operator < (const line &b) const
{
return w < b.w;
}
}l[maxm],mst[maxn];

struct edge
{
int u,v,next;
}e[maxm];
int h[maxn],num;
int fa[maxn];
long long a[maxn];
int n,m,k,cnt;
long long ans;

void build(int no,int u,int v)
{
num++;
e[num].u = u;
e[num].v = v;
e[num].next = h[no];
h[no] = num;
}

int getfather(int x)
{
if(x == fa[x])
return x;
else
return fa[x] = getfather(fa[x]);
}

int main()
{
freopen("airplane.in","r",stdin);
freopen("airplane.out","w",stdout);
scanf("%d%d%d",&n,&m,&k);
for(int i = 1; i <= m; i++)
scanf("%I64d",&a[i]);
int u,v,no,w;
for(int i = 1; i <= k; i++)
{
scanf("%d%d%d%d",&u,&v,&w,&no);
l[i].u = u;
l[i].v = v;
l[i].w = w;
build(no,u,v);
}
sort(l+1,l+1+k);
for(int i = 1; i <= n; i++)
fa[i] = i;
for(int i = 1; i <= k; i++)
{
int x = getfather(l[i].u);
int y = getfather(l[i].v);
if(x != y)
{
fa[y] = x;
mst[++cnt] = l[i];
}
}
ans = INF;
for(int i = 1; i <= m; i++)
{
for(int j = 1; j <= n; j++)
fa[j] = j;
for(int j = h[i]; j; j = e[j].next)
{
int x = getfather(e[j].u);
int y = getfather(e[j].v);
if(x != y)
fa[y] = x;
}
long long res = 0;
for(int j = 1; j <= cnt; j++)
{
int x = getfather(mst[j].u);
int y = getfather(mst[j].v);
if(x != y)
{
fa[y] = x;
res += mst[j].w;
}
}
if(res + a[i] < ans)
ans = res + a[i];
}
printf("%I64d\n",ans);
return 0;
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐