1. 程式人生 > 實用技巧 >運輸計劃

運輸計劃

運輸計劃

首先熟悉一下題目:

在一棵有邊權的樹(n個節點)上有m條路徑,清零一條邊的邊權使得m條路徑的最大值最小

至於資料範圍

20分

m=1

好啦,好像可做,一眼望去全是水

只需求出一條鏈上的所有邊並計算邊權和及最大邊權(暴力往上跳並記錄即可)

邊權和減去最大邊權即為答案

那麼我們就可以O(n)過掉這道題了(不嫌麻煩的話也可以O(log n)搞樹上路徑)

60分?

從未如此接近滿分

全是鏈,這意味著什麼(並不意味著什麼)

想了想,發現好像60分並不好搞

考慮一下暴力吧

超級暴力:暴力列舉刪每一條邊,統計刪完這條邊之後最長鏈的長度,取最小值就是答案,複雜度O(n^2 m),25分

剛才的小優化

考慮優化暴力

列舉刪哪條邊O(n)顯然已經達到理論下限

如果非要搞它的話只能排除那些不被經過的邊,效率高不了多少

接下來是統計每條鏈的長度

全是鏈哎,求線性區間和,字首和優化,消去一個O(n)

那個O(m)好像沒有什麼有效的優化

這樣,複雜度降至O(nm),40分

然後其他資料,搞樹鏈剖分動態修改、查詢可以多拿一些分,複雜度O(nm log n),60分

怎麼辦

QAQ,60分都拿不到了嗎

可不可以不實際改邊權呢?

經過不會就猜二分

經過深入思考,我們發現:

最短時間為t,前提是對於length>t的所有鏈,總能找到至少一條長為k公共邊,使得最長鏈的長度max length-k<=t

如果知道答案,好像不僅不用列舉最長鏈,還可以把列舉刪邊變為貪心刪掉被全部滿足條件的鏈經過的最長邊,穩賺一個O(n)和一個O(m)

考慮二分答案

如果能夠在時間t1內完成任務,那麼對於t2>t1,總能在時間t2內完成任務

所以答案符合單調性

可以二分答案

Check函式怎麼寫呢,看一看能不能找到找到至少一條長為k公共邊,使得最長鏈的長度max length-k<=t

設length>t的邊的個數為number

我們必須知道一條邊是否曾被number個鏈同時經過,唯一的方法好像就是差分了,check函式可以寫成O(n + m)的,總複雜度O((n + m)log n),60分

100分

二分答案的做法放到樹上呢

考慮線性資料上二分的完整做法

預處理每一條鏈的length,二分答案,放到check函式裡搞

沒問題

LCA求出每條鏈的length,還是二分,check函式換成樹上差分

最後發現正解只要一句話:

求鏈長+二分

存圖

存樹

struct edge{
	int v,nxt,w;
}e[maxx<<1],q[maxx<<1];

inline void add_(int u,int v,int w){
	 e[++js].v = v;
	 e[js].w = w;
	 e[js].nxt = head[u];
	 head[u] = js;
}

存每一條路徑(方便樹上差分)

struct length{
	int len,lca,u,v;//儲存每一條路徑的長度,lca,起點和終點 
}len[maxx];

inline void addque(int u,int v){//對所求路徑建圖 
	 q[++js].v = v;
	 q[js].nxt = headt[u];
	 headt[u] = js;
}

並查集

inline int find(int x){
	if(f[x]==x) return  x;
	else
    return f[x] = find(f[x]);
}

tarjan求每條鏈的長度,lca,以及最長鏈的len

void tarjan(int u,int pre){//pre前驅,防止走到自己 
    for (int i = head[u];i;i = e[i].nxt){
    	 int v  = e[i].v;
    	 if(v == pre)//如果下一個點是自己的前驅就跳過 
    	  continue;
    	 dis[v] = dis[u] + e[i].w;//存下每個點到原點的距離 
		 tarjan(v,u);
		 a[v] = e[i].w;//連到v這個點的上一條邊的權值;
		 int f1 = find(v);
		 int f2 = find(u);
		 if(f1!=f2)
		  f[f1] = find(f2);//存公共最先 
		  vis[v] = 1;
	}
   for (int i = headt[u];i;i = q[i].nxt){
  	  if(vis[q[i].v]){
  	    int p = (i + 1)>>1;
		 len[p].lca = find(q[i].v);//塔尖求lca 
                 len[p].len = dis[u]+dis[q[i].v]-2*dis[len[p].lca];//求鏈的長度(兩點到原點距離-兩點lca的dis*2) 
		 maxlen = max(maxlen,len[p].len);//存下最長鏈,二分答案的時候用	
	  }
  }

樹上差分

inline bool check(int x){
    memset(s,0,sizeof(s));
     num = ret = 0;//ret代表多條路徑重複部分最長的邊 
    for(int i = 1; i <= m; i++)
    	 if(len[i].len>x){//樹上差分
    	 	s[len[i].u]++;
    	 	s[len[i].v]++;
    	 	s[len[i].lca]-= 2;
    	 	num++;//記錄len>x的鏈的個數 
		 }
  dfs(1,0);
  if(maxlen-ret>x)
     return 0;
  return 1;
}
void dfs(int u,int pre){
	 for(int i = head[u];i;i = e[i].nxt){
	 	int v = e[i].v;
	 	if(v == pre)
	 	continue;
	 	dfs(v,u);
	 	s[u]+=s[v];
	 }
	 if(s[u]==num&&a[u]>ret)
	 ret = a[u];
}

node

/*
work by:Ariel
*/
#include <iostream>
#include <cstdio>
#include <queue>
#include <algorithm>
#include <cstring>
using namespace std;
const int maxx = 3e5 + 10;
struct edge{
	int v,nxt,w;
}e[maxx<<1],q[maxx<<1];
struct length{
	int len,lca,u,v;//儲存每一條路徑的長度,lca,起點和終點 
}len[maxx];
int js,head[maxx],headt[maxx];//存圖所用的變數
int dis[maxx];//記錄路徑 
int f[maxx];//並查集
int a[maxx];//這個點前一條邊的權值
int vis[maxx],maxlen; 
int s[maxx];
int num,ret;
int n,m,ans;
inline void add_(int u,int v,int w){//建圖 
	 e[++js].v = v;
	 e[js].w = w;
	 e[js].nxt = head[u];
	 head[u] = js;
}
inline void addque(int u,int v){//對所求路徑建圖 
	 q[++js].v = v;
	 q[js].nxt = headt[u];
	 headt[u] = js;
}

inline int find(int x){
	if(f[x]==x) return  x;
	else
    return f[x] = find(f[x]);
}
void tarjan(int u,int pre){//pre前驅,防止走到自己 
    for (int i = head[u];i;i = e[i].nxt){
    	 int v  = e[i].v;
    	 if(v == pre)//如果下一個點是自己的前驅就跳過 
    	  continue;
    	 dis[v] = dis[u] + e[i].w;//存下每個點到原點的距離 
		 tarjan(v,u);
		 a[v] = e[i].w;//連到v這個點的上一條邊的權值;
		 int f1 = find(v);
		 int f2 = find(u);
		 if(f1!=f2)
		  f[f1] = find(f2);//存公共最先 
		  vis[v] = 1;
	}
  for (int i = headt[u];i;i = q[i].nxt){
  	  if(vis[q[i].v]){
  	    int p = (i + 1)>>1;
		 len[p].lca = find(q[i].v);//塔尖求lca 
		 len[p].len = dis[u]+dis[q[i].v]-2*dis[len[p].lca];//求鏈的長度(兩點到原點距離-兩點lca的dis*2) 
		 maxlen = max(maxlen,len[p].len);//存下最長鏈,二分答案的時候用	
	  }
  }
}
void dfs(int u,int pre){
	 for(int i = head[u];i;i = e[i].nxt){
	 	int v = e[i].v;
	 	if(v == pre)
	 	continue;
	 	dfs(v,u);
	 	s[u]+=s[v];
	 }
	 if(s[u]==num&&a[u]>ret)
	 ret = a[u];
}
inline bool check(int x){
    memset(s,0,sizeof(s));
     num = ret = 0;//ret代表多條路徑重複部分最長的邊 
    for(int i = 1; i <= m; i++)
    	 if(len[i].len>x){//樹上差分
    	 	s[len[i].u]++;
    	 	s[len[i].v]++;
    	 	s[len[i].lca]-= 2;
    	 	num++;//記錄len>x的鏈的個數 
		 }
  dfs(1,0);
  if(maxlen-ret>x)
     return 0;
  return 1;
}
int main()
{
  scanf("%d%d",&n,&m);
  for (int i = 1;i < n; i++){
  	  int u,v,w;
  	  scanf("%d%d%d",&u,&v,&w);
  	  add_(u,v,w);
  	  add_(v,u,w);
  }
  for (int i  = 1; i <= n; i++)
     f[i] = i;//把每一個點的父親設為自己(並查集初始化) 
     js = 0;
  for (int i = 1 ;i <= m; i++){
  	   int x,y;
  	   scanf("%d%d",&x,&y);
	   len[i].u = x;
	   len[i].v = y;
	   addque(x,y);
	   addque(y,x); 
  }
   tarjan(1,0);
   int l = 0,r = maxlen;
   while(l <= r){
   	  int mid = (l+r)>>1;//二分 
   	  if(check(mid)){
   	  	  r = mid - 1;
   	  	  ans = mid;
		 }
	  else l = mid + 1;
   }
   printf("%d",ans);	  
  return 0;
}