[Round1 开学信心赛] 越狱

题外话:

EZEC官方题解:

有点桑心啊。。。这次除了内部人员居然没人AC这题,搞了好久的说。

唯一一个35分的代码还是写的无优化LCA解法,而且他思路我还没看懂

被吐槽除了算法之外有点裸,这里讲一下:这题原本的想法其实是后面讲到的正解第二种(虽然当时没有想到tarjan),但是原本的思路并没有第一种正解那么裸


对于 \(N \le 10\) 的数据,实时记录小E和PF到每个岛的距离,然后 \(dfs\) 枚举每种可能的走法。复杂度 \(N!\)


对于 \(N \le 16\) 的数据,考虑状压dp。\(dp[i][j]\) 记录当前的状态以及当前所在的点。状压完后对每个状态进行统计。如果状态数超过 \(l\) 并且小于当前所能得到的最小值 ,那么就更新答案。

在状压过程中更新答案会更快但这个跟正解没啥关系

复杂度为 \(n*2^n\),可以过。

代码略(状压你都会你跟我讲你想不出正解?


当数字变大后,指数级别的答案肯定不合法。因此,我们需要一个更快的方法。

第一步观察到狱长的最短距离在加边前后都是一个固定值(未必相等)。因此,我们可以预处理这个距离,加边,然后再预处理一次。

可以发现,由于只有 \(N-1\) 条边,在加边之前每一条边到另一条边都有唯一的最短路。

\(N\le500\) 的数据时,我们可以对每个点跑一次 \(dij\),预处理出每个点到其他任意点的距离和长度,再进行加边操作。狱长只能加一次边的操作我们可以使用分层图来实现。

加完边后,暴力枚举 \(K\),取最大的 \(K\) 作为答案。

暴力枚举的部分代码:

//maxi为最大的p_i和e_i的值
inline void find_ans(){
  int ans1=-1,ans2;
  for (int i=1;i<=maxi;i++){
    int re = dijk(1,i);
    if(re>=L) ans1 = i,ans2 = re;
  }
  if (ans1==-1) cout << "no solution";
  else cout << ans1 << endl << ans2;
}

这个算法复杂度为 \(N^2 \times Max(p_i,e_i)\),大约为 \(500^3\),可以在规定时间内跑完。

另一种方法为保留到每条边的最小 \(K\) 值,预处理后对整张图统计一次最小答案,这里留给读者自行思考。


\(N \le2500\) 的时候,我们需要一个更加简单的方法。

我们可以发现,既然每两点只有一条边互通,那么他们只有一个公共祖先。他们之间的最短距离可以表示为两点分别到公共祖先的距离的和,他们之间的点的数量可以表示为两个点的深度减去祖先的深度。这个操作可以使用 \(LCA\) 来实现。

因此,我们只需要从点 \(1\) 开始跑一个 \(dij\) ,然后对于每两点判断一下是否能够加边。

加完边后,我们可以二分 \(K\),如果小 E 能够跑到 \(L\) 个点上,并且到每个点的距离都比狱长小,那么这个 \(K\) 是可实现的。

这个做法的复杂度为 \(N^2logN\) ,可以过 \(N \le 2500\) 的数据

#include <iostream>
#include <algorithm>
#include <vector>
#include <queue>
#include <fstream>
#include <cstring>
#include <math.h>
using namespace std;
const int MAXN = 5e3+5;
#define pp pair<long long,long long>
#define f first
#define s second
const int BD = 14;
int n,m,t,L,q,d,level[MAXN<<1],pa[MAXN<<1][BD];
inline long long read() {
  long long x=0,w=1;
  char ch;
  while(ch<'0'||ch>'9'){
    if(ch=='-') w=-1;
    ch=getchar();
  }
  while(ch>='0'&&ch<='9')
  x=(x<<3)+(x<<1)+ch-'0',ch=getchar();
  return x*w;
}
bool inq[MAXN<<1];
vector<pp> adj[MAXN<<1],adj2[MAXN<<1];
long long dist1[MAXN<<1],dist2[MAXN<<1],dist3[MAXN];
int LCA(int u, int v) {
	if(level[u] < level[v]) swap(u,v);
	int diff = level[u] - level[v];
	for(int i=0; i<BD; i++) if((diff>>i)&1 ) u = pa[u][i];
	if(u == v) return u;
	for(int i=BD-1; i>=0; i--) if(pa[u][i] != pa[v][i]) {
		u = pa[u][i];
		v = pa[v][i];
	}
	return pa[u][0];
}//找LCA的板子
void set_up_lca(int nn){
  for(int i = 1 ; i <= BD ; i++){
    for(int j = 0; j < nn ; j++){
      if(pa[j][i-1] != -1) pa[j][i] = pa[pa[j][i-1]][i-1] ;
    }
  }
}//lca的板子
void dfs(int pos, int lev, int prev){
  pa[pos][0] = prev;
  level[pos] = lev;
  for (pp v : adj[pos]){
    if (v.f==prev) continue;
    dfs(v.f,lev+1,pos);
  }
}//把深度找出来
inline void dij(int source, vector<pp> adja[MAXN], long long dist[MAXN]){
  for (int i=0;i<=2*n;i++) dist[i] = 1e9;
  queue<int> q;
  dist[source] = t;
  q.push(source);
  while(!q.empty()){
    long long qs = q.front(); q.pop();
    inq[qs] = false;
    for (pp v : adja[qs]){
      if (dist[v.f]>dist[qs]+v.s){
        dist[v.f]=dist[qs]+v.s;
        if (!inq[v.f]) inq[v.f] = true,q.push(v.f);
      }
    }
  }
}//预处理距离
inline void add_edge(){
  for (int i=1;i<=n;i++){
    for (int j=i+1;j<=n;j++){
      if(i==j)continue;
      int lca = LCA(i,j);
      long long dis = dist2[i]+dist2[j]-2*dist2[lca];
      if (dis<=d && level[i]+level[j]-2*level[lca]-1>=q){//如果他们距离小于D且之间有超过Q个点
        adj2[i].push_back(make_pair(j+n,floor(dis/2)));
        adj2[j].push_back(make_pair(i+n,floor(dis/2)));
      }
    }
  }
}
bool seem[MAXN];
inline int dijk(int source, long long num){
  memset(dist1,0x3f3f3f,sizeof(dist1));
  memset(seem,0,sizeof(seem));
  queue<int> q;
  int re = 0;
  dist1[source] = 0;
  q.push(source);
  while(!q.empty()){
    long long qs = q.front(); q.pop();
    inq[qs] = false;
    if (!seem[qs]) seem[qs] = true,re++;
    for (pp v : adj[qs]){
      if (v.s>num) continue;//如果这条边的距离用K足够
      if (dist1[v.f]>dist1[qs]+v.s && dist1[qs]+v.s<=dist3[v.f]){//如果
        dist1[v.f]=dist1[qs]+v.s;
        if (!inq[v.f])inq[v.f] = true,q.push(v.f);
      }
    }
  }
  return re;
}
inline void bs(){
  long long l = 0, r = 1e18;
  while(l!=r-1){
    long long mid = (l+r)/2;
    if (dijk(1,mid)>=L) r = mid;
    else l = mid;
  }//二分
  if (dijk(1,l)>=L) cout << l << endl << dijk(1,l);
  else if (dijk(1,r)>=L) cout << r << endl << dijk(1,r);
  else cout << "no solution";
}
inline void update_dis(){
  for (int i=1;i<=n;i++) dist3[i] = min(dist2[i],dist2[i+n]);
}

int u,v,p,e;
int main(){
  n = read(); t= read(); d = read(); L = read();q = read();
  for (int i=0;i<n-1;i++){
    u = read(); v = read(); p = read(); e = read();
    adj[u].push_back(make_pair(v,p));
    adj[v].push_back(make_pair(u,p));
    adj[u+n].push_back(make_pair(v+n,p));
    adj[v+n].push_back(make_pair(u+n,p));
    adj2[u].push_back(make_pair(v,e));
    adj2[u+n].push_back(make_pair(v+n ,e));
    adj2[v].push_back(make_pair(u,e));
    adj2[v+n].push_back(make_pair(u+n ,e));//别忘了双向边和分层图边的加法
  }
  for (int i=1;i<(MAXN<<1);i++){
    for (int j=0;j<BD;j++){
      pa[i][j] = -1;
    }
  }
  dfs(1,0,-1);
  set_up_lca(n*2);//预处理公共祖先
  dij(1,adj2,dist2);//预处理距离
  add_edge();//加边
  dij(1,adj2,dist2);//再预处理距离
  update_dis();//更新距离
  bs();//二分
}

另一种更简单的思维方法是暴力枚举节点进行 \(dijkstra\) ,这里留给读者自行思考


\(N\le 7500\) 的时候,\(N^2logN\) 会直接被卡掉。

这时候,我们需要一个更简便的方法。

这时候,万能的 \(dfs\) 就出来了。

暴力枚举每个节点,顺着边往下走。当距离超过 \(D\) 时就返回,否则如果中间岛屿超过 \(Q\) 就加边。

#include<cstdio>
#include<iostream>
#include<algorithm>
#include<cstring>
#include<queue>
#include <math.h>
using namespace std;
#define G() Cr=getchar()
#define LL long long
LL Xr,Fr;char Cr;
int bian;
inline LL rd(){
	Xr=0,Fr=1;G();
	while(Cr<'0'||Cr>'9'){if(Cr=='-')Fr=-1;G();}
	while(Cr>='0'&&Cr<='9')Xr=(Xr<<1)+(Xr<<3)+Cr-'0',G();
	return Xr*Fr;
}
#define MAX_N 15000
#define MAX_M 10000000
#define oo 9999999999999999
int n, T, D, L, Q;
LL dis[MAX_N], Dis[MAX_N];
LL va[MAX_N], Va[MAX_M];
int to[MAX_N], ne[MAX_N], he[MAX_N], cnt;
int To[MAX_M], Ne[MAX_M], He[MAX_M], Cnt;
int add_u[MAX_M], add_v[MAX_M], add_t;
LL add_d[MAX_M];
int a, b;
LL c, d;
bool vis[MAX_N];
LL ans, Ans, Ma, num;

void add(int u, int v){
	to[++cnt]=v;
	ne[cnt]=he[u];
	he[u]=cnt;
	va[cnt]=c;
}

void Add(int u, int v){
	To[++Cnt]=v;
	Ne[Cnt]=He[u];
	He[u]=Cnt;
	Va[Cnt]=d;
}

void dfs_ae(int U, int u, LL dd, int t, int la){
	if(dd>D)return;
	if(t>=Q) add_u[++add_t]=U, add_v[add_t]=u, add_d[add_t]=floor(dd/2);
	for(int i=He[u];i;i=Ne[i]) if(To[i]!=la && To[i]<=n) dfs_ae(U,To[i],dd+Va[i],t+1,u);
}//预处理

void Dijk(int U){
	priority_queue< pair<int,int> >q;
	q.push(make_pair(0,U));
	for(int i=1;i<=2*n;i++) Dis[i]=oo;
	Dis[U]=0;
	while(!q.empty()){
		int u=q.top().second;
		if (-q.top().first>Dis[u]) {q.pop(); continue;}
		q.pop();
		vis[u]=1;
		for(int i=He[u];i;i=Ne[i]){
			int v=To[i];
			if(vis[v])continue;
			if(Dis[v]>Dis[u]+Va[i]){//跟之前同样的操作
				Dis[v]=Dis[u]+Va[i];
				q.push(make_pair(-Dis[v],v));
			}
		}
	}
}
bool seem[MAX_N];
void dijk(int t){
	memset(seem,0,sizeof(seem));
	priority_queue< pair<int,int> >q;
	q.push(make_pair(0,1));
	for(int i=1;i<=n;i++) dis[i]=oo, vis[i]=0;
	dis[1]=0;
	while(!q.empty()){
		int u=q.top().second;
		q.pop();
		num++;
		seem[u] = true;
		for(int i=he[u];i;i=ne[i]){
			int v=to[i];
			if(seem[v])continue;
			if(dis[v]>dis[u]+va[i] && dis[u]+va[i]<=T+Dis[v] && va[i]<=t){
				dis[v]=dis[u]+va[i];
				q.push(make_pair(-dis[v],v));
			}
		}
	}
}

bool check(int t){
	num=0;
	dijk(t);
	if(num>=L){
		Ans=num;
		return 1;
	}
	else return 0;
}//某数字是否能实现

int main(){
	cin>>n>>T>>D>>L>>Q;
	for(int i=1;i<n;i++){
		a=rd(), b=rd(), c=rd(), d=rd();
		Ma=max(Ma,c);
		if(c) add(a,b), add(b,a);
		if(d) Add(a,b), Add(b,a), Add(a+n,b+n), Add(b+n,a+n);
	}
	for(int i=1;i<=n;i++) dfs_ae(i,i,0,-1,0);//预处理狱长距离
	for(int i=1;i<=add_t;i++) {
		d=add_d[i], Add(add_u[i],add_v[i]+n);}//加边
	Dijk(1);//二次处理
	for(int i=1;i<=n;i++)Dis[i]=min(Dis[i],Dis[i+n]);//更新距离
	ans=2147483647;
	int l=0, r=Ma;
	while(l<=r){
		int mid=(l+r)>>1;
		if(check(mid)) ans=mid, r=mid-1;
		else l=mid+1;
	}//二分套路
	if(ans==2147483647)puts("no solution");
	else cout<<ans<<endl<<Ans;
}

正解其实并不比75分做法难想。然而,这题有一个玄学的地方:

void dfs_ae(int U, int u, LL dd, int t, int la){
	if(dd>D)return;
	if(t>=Q) add_u[++add_t]=U, add_v[add_t]=u, add_d[add_t]=floor(dd/2);//这行
	for(int i=He[u];i;i=Ne[i]) if(To[i]!=la && To[i]<=n) dfs_ae(U,To[i],dd+Va[i],t+1,u);
}

看起来确实像是多此一举。貌似还多开了一个数组。

然而,如果将这里改成这样:

void dfs_ae(int U, int u, LL dd, int t, int la){
	if(dd>D)return;
	d = floor(dd/2);
	Add(U,u);
	// if(t>=Q) add_u[++add_t]=U, add_v[add_t]=u, add_d[add_t]=floor(dd/2);
	for(int i=He[u];i;i=Ne[i]) if(To[i]!=la && To[i]<=n) dfs_ae(U,To[i],dd+Va[i],t+1,u);
}

就会惊奇的发现获得了70分的成绩。虽然总复杂度不变,但是如此建边有一个特点:每次 dfs 边加边,会导致所有边都被枚举了一次,最后复杂度可能变为 \(O(N(N+M))\)\(M\) 为加边条数,导致时间复杂度爆炸。

这种解法复杂度为 \(N^2\),能够在规定时间跑完。


另一种较为复杂的方法为 \(LCA\) \(+\) \(tarjan\) 。我们可以发现,如果从树的顶端往下跑,那么当某两个点在同一棵树内,他们的祖先的祖先对他没有任何意义。同理,如果从下往上跑,那么子树对于父节点也没有任何意义。于是,当我们处理完子树,我们可以直接合并子树的祖先。(其实只能说类似tarjan

#include <iostream>
#include <algorithm>
#include <vector>
#include <queue>
#include <fstream>
#include <cstring>
#include <math.h>
using namespace std;
const int MAXN = 7e3+505;
#define pp pair<long long,long long>
#define f first
#define s second
const int BD = 12;
const int MAXB = 10000000;
int n,m,t,L,q,ed,d,level[MAXN<<1];
inline long long read() {
	long long x=0,w=1;
	char ch;
	while(ch<'0'||ch>'9'){
		if(ch=='-') w=-1;
		ch=getchar();
	}
	while(ch>='0'&&ch<='9')
	x=(x<<3)+(x<<1)+ch-'0',ch=getchar();
	return x*w;
}
struct Edge{
  int t,d,nxt;
}edge[MAXB],edge2[MAXB];
int head[MAXN],head2[MAXN<<1],tot,tot2;
inline void add(int from, int to, int dis){
  edge[++tot].t = to;
  edge[tot].d = dis;
  edge[tot].nxt = head[from];
  head[from] = tot;
}
inline void add2(int from, int to, int dis){
  edge2[++tot2].t = to;
  edge2[tot2].d = dis;
  edge2[tot2].nxt = head2[from];
  head2[from] = tot2;
}
bool inq[MAXN<<1];
long long dist1[MAXN],dist2[MAXN<<1],dist3[MAXN];
void dfs(int pos, int lev, int prev){
	level[pos] = lev;
	for(int i = head[pos];i;i=edge[i].nxt){
    int to =edge[i].t;
		if (to==prev) continue;
		dfs(to,lev+1,pos);
	}//同样写lca的配方
}
inline void dij(int source){
  fill(dist2+1,dist2+1+2*n,1e18);
	queue<int> q;
	dist2[source] = t;
	q.push(source);
	while(!q.empty()){
		long long qs = q.front(); q.pop();
		inq[qs] = false;
    for (int i=head2[qs];i;i=edge2[i].nxt){
      int to = edge2[i].t,d = edge2[i].d;
      if (dist2[to]>dist2[qs]+d){
        dist2[to]=dist2[qs]+d;
        if (!inq[to])inq[to] = true,q.push(to);
      }
    }
	}
}//还是同样的方法
int father[MAXN];
int LCA(int x){
	return father[x]^x?father[x]=LCA(father[x]):x;
}//lca的方法
int bian = 0;
bool visit[MAXN];
inline void add_edge(int i,int fa){
	father[i]=i;
	for(int j=head2[i];j;j=edge2[j].nxt){
    int to = edge2[j].t;
		if(to^fa && !visit[to]){
			add_edge(to,i);
			father[to]=i;
		}//先建子树,然后合并
	}
	visit[i]=true;
	for (int j=1;j<=n;j++){
		if(i==j || !visit[j])continue;
		int lca = LCA(j);
		long long dis = dist2[i]+dist2[j]-(dist2[lca]<<1);
		if (dis<=d && level[i]+level[j]-(level[lca]<<1)-1>=q){
			bian+=2;
      add2(i,j+n,dis/2);
      add2(j,i+n,dis/2);
		}
	}//建边
}
bool seem[MAXN];
inline int dijk(int source, long long num){
	memset(dist1,0x3f3f3f,sizeof(dist1));
	memset(seem,0,sizeof(seem));
	queue<int> q;
	int re = 0;
	dist1[source] = 0;
	q.push(source);
	while(!q.empty()){
		long long qs = q.front(); q.pop();
		inq[qs] = false;
		if (!seem[qs]) seem[qs] = true,re++;
		for(int i = head[qs];i;i=edge[i].nxt){
      int to = edge[i].t,d = edge[i].d;
			if (d>num) continue;
			if (dist1[to]>dist1[qs]+d && dist1[qs]+d<=dist3[to]){
				dist1[to]=dist1[qs]+d;
				if (!inq[to])inq[to] = true,q.push(to);
			}
		}
	}
	return re;
}//二分dij
inline void bs(){
	long long l = 0, r = 1e18;
	while(l!=r-1){
		long long mid = (l+r)/2;
		if (dijk(1,mid)>=L) r = mid;
		else l = mid;
	}
	if (dijk(1,l)>=L) cout << l << endl << dijk(1,l);
	else if (dijk(1,r)>=L) cout << r << endl << dijk(1,r);
	else cout << "no solution";
}//二分
inline void update_dis(){
	for (int i=1;i<=n;i++) dist3[i] = min(dist2[i],dist2[i+n]);
}//更新
int u,v,p,e;
int main(){
	n = read(); t= read(); d = read(); L = read();q = read();
	ed = n;
	for (int i=0;i<n-1;i++){
		u = read(); v = read(); p = read(); e = read();
    add(u,v,p);
    add(v,u,p);
    add2(u,v,e);
    add2(v,u,e);
    add2(u+n,v+n,e);
    add2(v+n,u+n,e);
	}
	dfs(1,0,-1);
	dij(1);
	add_edge(1,-1);
	dij(1);
	update_dis();
	bs();//这里方法基本与上面一样
}

部分分方法:

对于 \(Q=0\) 的点,我们可以直接两两判断时间是否超越 \(D\),并不需要判断之间的距离。期望得分 \(35\),实际得分 \(45\)

对于不加边的做法,我们就不加边,不使用分层图,预计得分 \(20\),实际得分 \(20\)


对于大常数选手(比如我),我们给与了足够大的空间和时间( \(2\) 倍空间时间)。但由于要卡 \(75\) 分做法,如果正解常数巨大的话无法满分。

posted @ 2020-04-22 14:38  pocafup  阅读(170)  评论(0编辑  收藏  举报