GYM 100726F 解题思路分析
题目大意:
给出一棵树,你需要访问这棵树上的某些结点几次,求出使访问这些结点路线最短的那个点所需要的最短路径长度并列出每一个符合条件的点。
思路过程:
一开始看到这是一棵树并要求我求出树上两点之间的距离的时候,首先想到的就是ST算法的RMQ实现的LCA求树上两点距离。但是由于当时没有看好M(既需要访问的点的数量)的范围,于是就接着写下去了,但是虽然处理出RMQ只需要NlogN,但是统计答案却要N^2所以说它很自然的就超时了。
那么接着想,这个算法超时在哪个部分呢?统计答案,既每一个结点到目的结点的距离之和。要如何进行优化呢?自己手模了两三组数据之后发现,统计答案的时候很多的计算都是冗余的,因为在原来的程序中,统计每一个结点的代价和的时候都要把每一个需要去访问的结点全部循环统计一遍。但是这个状态貌似是可以推移得出的。
但是又自己想了一会儿之后,会发现这样一个规律:如果居住的这个点向其他的它所临的点推移的话,假如推移之前的点为u,推移到v点。那么进行这个转移的时候,就可以把一棵树分为有两种情况的两棵树:
从图中可以看出如果把家从u挪到v去的话,就可以把这个原图中的树分为两部分去考虑,既以u为根但不包含v的子树那一部分与以v为根的子树那部分。把家从u搬到v,对于左半部分的子树来说,每一个结点到家的距离都加上了cost(u、v两节点之间的边权);而对于右半部分的子树(包括v结点)来说,每一个结点到家的距离都减少了cost。
这条规律就成为了解决这道题的突破口,只要先处理出以1结点为家的情况,再向其他的结点以O1的复杂度进行推移就可以枚举到所有可能的解。
代码实现:
已经知道了思路之后,就应该考虑如何处理出需要的数据。
刚刚的思路中已经写道,把家从一个点转移到另一个结点的过程需要把这棵树分为两部分,而上述的蓝色的两部分也等价于以v为根的子树与这棵子树在全图中的补集。推移的过程中需要的只是从u到v这条路径在两部分影响中出现的次数。换句话说,以v为根的子树这个集合中的每一个结点到家1次的距离都少了cost,这棵子树在全图中的补集这个集合的每一个结点到家1次的距离都多了cost。我们想转移信息,就要知道少了几个cost,多个几个cost。也就是说需要求出这两个集合中的点分别需要被访问的次数。
可以看出,那些被列出的需要访问的结点都有该结点对应的访问次数。那么我们可以把结点1处理为根节点,在firstDfs函数中处理出从1出发的条件下,每一个结点需要被到达的次数,而每一个结点需要被到达的次数就等于它的所有子孙结点应该被到达的次数的总和。因为如果要到达一个节点的子孙结点的话,该结点是“必经之路”。
#include<cstdio>
#include<cstdlib>
#include<cmath>
#include<algorithm>
#include<string>
#include<cstring>
#include<iostream>
#include<vector>
#include<queue>
#define min(a,b) ((a)<(b)?(a):(b))
#define max(a,b) ((a)>(b)?(a):(b))
#define abs(a) ((a)<(0)?-(a):(a))
using namespace std;
#define mp make_pair
const int maxn=50005;
int n,m;
int vis[maxn];
int vis_sum[maxn];
long long dis[maxn];
long long dis_sum[maxn];
vector < pair<int,int> > eage[maxn];
vector <int> ans;
void dfs1(int u,int p)
{
vis_sum[u]=vis[u];
for(int i=0;i<eage[u].size();i++) if(eage[u][i].first!=p)
{
int v=eage[u][i].first;
dis[v]=dis[u]+eage[u][i].second;
dfs1(v,u);
vis_sum[u]+=vis_sum[v];
}
}
void dfs2(int u,int p)
{
for(int i=0;i<eage[u].size();i++) if(eage[u][i].first!=p)
{
int v=eage[u][i].first;
dis_sum[v]=dis_sum[u]-1LL*vis_sum[v]*eage[u][i].second+1LL*(vis_sum[1]-vis_sum[v])*eage[u][i].second;
dfs2(v,u);
}
}
void work()
{
memset(vis,0,sizeof vis);
memset(dis,0LL,sizeof dis);
memset(dis_sum,0LL,sizeof dis_sum);
memset(vis_sum,0,sizeof vis_sum);
scanf("%d",&n);
for(int i=1;i<=n;i++) eage[i].clear();
for(int i=1;i<n;i++)
{
int x,y,z;
scanf("%d%d%d",&x,&y,&z);
eage[x].push_back(mp(y,z));
eage[y].push_back(mp(x,z));
}
scanf("%d",&m);
for(int i=1;i<=m;i++)
{
int x,y;
scanf("%d%d",&x,&y);
vis[x]=y;
}
dfs1(1,-1);
for(int i=1;i<=n;i++) dis_sum[1]+=1LL*dis[i]*vis[i];
dfs2(1,-1);
ans.clear();
long long best=(1LL<<62);
for(int i=1;i<=n;i++)
{
if(dis_sum[i]<best)
{
best=dis_sum[i];
ans.clear();
ans.push_back(i);
}
else if(dis_sum[i]==best)
ans.push_back(i);
else
continue;
}
printf("%I64d\n",best*2);
for(int i=0;i<ans.size();i++) printf("%d ",ans[i]);
printf("\n");
}
int main()
{
int cas;
scanf("%d",&cas);
for(int i=1;i<=cas;i++) work();
return 0;
}
又由于每个点的信息是“推移”得到的,所以这仍然要用递归的形式来实现。整份代码时间复杂度目测为N。
看来的确没错:
21612695 2016-12-26 11:53:27 Ukinojs 100726F - Moving to Nuremberg GNU C++ Accepted 264 ms 7800 KB