• [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) 条边,在加边之前每一条边到另一条边都有唯一的最短路。

    (Nle500) 的数据时,我们可以对每个点跑一次 (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 imes 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) ,这里留给读者自行思考


    (Nle 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) 分做法,如果正解常数巨大的话无法满分。

  • 相关阅读:
    java基础,集合,ConcurrentHashMap,JDK1.7理解
    单例的几种方式,以及如何破坏单例,使用枚举保护单例;
    测试一软件测试基础知识总结
    spring cloud gateway(三、实现限流)
    gateway(二、过滤器)
    spring cloud gateway
    spring cloud zuul网关
    进入mysql方法
    spring cloud consul 服务治理
    多模块项目之坑
  • 原文地址:https://www.cnblogs.com/DannyXu/p/12751943.html
Copyright © 2020-2023  润新知