NOIP 2015 運輸計劃 二分+差分 || 字首和O(n)
題目連結:運輸計劃
主要思路1:
(注:以下路徑耗時與路徑長度是同一回事)
由於本題求的是所有任務中所需時間最長的時間的最小值,故可以想到二分答案。二分所需的最長時間記為lim,check函式就找出所有任務所需時間超過lim的任務。並將其路徑用差分在樹上標記。可得知只有讓一條被所有不滿足任務路徑所覆蓋的路變成蟲洞才有可能使這個lim可行。故在這種邊中找一條最大的邊的長度記為mx,用MX(即為所有任務中所需時間最長的任務的所需時間)-mx與lim比較,若MX-mx>lim則不可行,若不存在這種邊也是不可行。反之則可行。(這樣子時間有點慢)
AC程式碼:
#include<cstdio> #include<cstring> #include<algorithm> #define M 300005 using namespace std; struct E{ int to,nx,d; }edge[M<<1]; int tot,head[M]; void Addedge(int a,int b,int d){ edge[++tot].to=b; edge[tot].nx=head[a]; edge[tot].d=d; head[a]=tot; } struct Que{ int st,ed,d; }Q[M]; int fa[M],sz[M],son[M],dep[M],dis[M],P[M],T,D[M]; void dfs(int now) { P[++T]=now; sz[now]=1; son[now]=0; for(int i=head[now]; i; i=edge[i].nx) { int nxt=edge[i].to; if(nxt==fa[now])continue; fa[nxt]=now; dep[nxt]=dep[now]+1; dis[nxt]=dis[now]+edge[i].d; D[nxt]=edge[i].d; dfs(nxt); sz[now]+=sz[nxt]; if(sz[nxt]>sz[son[now]])son[now]=nxt; } } int top[M]; void dfs_top(int now) {//跳重鏈求LCA if(son[now]) { top[son[now]]=top[now]; dfs_top(son[now]); } for(int i=head[now]; i; i=edge[i].nx) { int nxt=edge[i].to; if(nxt==fa[now]||nxt==son[now])continue; top[nxt]=nxt; dfs_top(nxt); } } int LCA(int a,int b) { while(top[a]!=top[b]) { if(dep[top[a]]<dep[top[b]])b=fa[top[b]]; else a=fa[top[a]]; } return dep[a]<dep[b]?a:b; } int lca[M]; int get_dis(int a,int b,int x) { return dis[a]+dis[b]-2*dis[x]; } int cnt[M],mx,Mx,ner; bool ok; void redfs(){ for(int i=T;i>=2;i--){//因為DFS常數太大,故用DFS序列舉即可 int now=P[i]; for(int j=head[now];j;j=edge[j].nx){ if(edge[j].to==fa[now])continue; cnt[now]+=cnt[edge[j].to]; } if(cnt[now]==ner)Mx=max(D[now],Mx),ok=1;//若他被所有不可行的路徑覆蓋 } } bool check(int X,int n,int m) { ner=0; memset(cnt,0,sizeof(cnt)); for(int i=1; i<=m; i++)if(Q[i].d>X)cnt[Q[i].st]++,cnt[Q[i].ed]++,cnt[lca[i]]-=2,ner++;//邊i表示其到達i這個點的邊 ok=0; Mx=0; redfs(); if(mx-Mx>X)return false; if(!ok)return false;//若不存在這樣的邊 return true; } void solve(int n,int m) { int l=0,r=mx,ans=0;//二分最大的路徑長度 while(l<=r) { int mid=(l+r)>>1; if(check(mid,n,m)) { ans=mid; r=mid-1; } else l=mid+1; } printf("%d\n",ans); } void Init(int n,int m) { top[1]=1; dfs(1); dfs_top(1); for(int i=1; i<=m; i++)lca[i]=LCA(Q[i].st,Q[i].ed);//預處理出LCA for(int i=1; i<=m; i++) { Q[i].d=get_dis(Q[i].st,Q[i].ed,lca[i]);//預處理出每條路徑的長度 mx=max(Q[i].d,mx); } } int main() { int n,m; scanf("%d%d",&n,&m); for(int i=1;i<n;i++){ int a,b,d; scanf("%d%d%d",&a,&b,&d); Addedge(a,b,d); Addedge(b,a,d); } for(int i=1;i<=m;i++)scanf("%d%d",&Q[i].st,&Q[i].ed); Init(n,m); solve(n,m); return 0; }
主要思路2:
進一步思考題目可以知道:
性質1:最後建設蟲洞的邊一定是在最長的路徑上的。
性質2:建設蟲洞後的答案為max(不經過這條蟲洞的最長路徑,最長的任務路徑-這條建設蟲洞的邊的長度,不與最長的路徑相交的路徑的最大值)。
知道以上性質這道題目就挺簡單了。
1.把任務中耗時最長的路徑(像直徑一樣)拉出來,並標號。(以下稱為直徑)
2.把直徑上每一個點向下DFS(不回到直徑),並標記每個點是從直徑上的哪個點下來的,存入KIND陣列中。
3.將一一列舉其他路徑,記路徑兩端點為的Kind值為x,y(x<=y).若x==y則這條路徑不與我們的直徑相交,記錄這種路徑長度的最大值。否則更新aft[x]=max(aft[x],這條邊的長度),pre[y]=max(pre[y],這條邊的長度)(aft[i]為路徑直徑上i這個點右邊的所有任務的耗時最大值,pre[i]為路徑在直徑上i這個點左邊所有任務的耗時最大值)
4.for一遍直徑上的所有點aft[i]=max(aft[i],aft[i+1]),pre[i]=max(pre[i],pre[i-1])(即預處理字首和)
5.一一列舉直徑上的點,(現在要建設蟲洞的邊為i->i+1)ans=min(ans,max(最長的任務路徑-這條建設蟲洞的邊的長度,不與最長的路徑相交的路徑的最大值,aft[i+1],pre[i]));
注意初始值ans=最長路徑的長度,否則碰到m==1&&這條邊起點和終點重合會掛。
以下有一張圖方便理解(圖有點醜,將就以下)
若還有不懂,可以自己畫圖試一試理解一下。
AC程式碼:
#include<cstdio> #include<algorithm> #define max4(a,b,c,d) max(max(a,b),max(c,d)) #define M 300005 using namespace std; struct E{ int to,nx,d; }edge[M<<1]; int tot,head[M]; void Addedge(int a,int b,int d){ edge[++tot].to=b; edge[tot].nx=head[a]; edge[tot].d=d; head[a]=tot; } struct line{ int st,ed,lca,d; }Q[M]; int fa[M],sz[M],son[M],dep[M],dis[M]; void dfs(int now){ son[now]=0; sz[now]=1; for(int i=head[now];i;i=edge[i].nx){ int nxt=edge[i].to; if(nxt==fa[now])continue; fa[nxt]=now; dep[nxt]=dep[now]+1; dis[nxt]=dis[now]+edge[i].d; dfs(nxt); sz[now]+=sz[nxt]; if(sz[son[now]]<sz[nxt])son[now]=nxt; } } int top[M]; void dfs_top(int now){//跳重鏈求LCA if(son[now]){ top[son[now]]=top[now]; dfs_top(son[now]); } for(int i=head[now];i;i=edge[i].nx){ int nxt=edge[i].to; if(nxt==fa[now]||nxt==son[now])continue; top[nxt]=nxt; dfs_top(nxt); } } int LCA(int a,int b){ while(top[a]!=top[b]){ if(dep[top[a]]<dep[top[b]])b=fa[top[b]]; else a=fa[top[a]]; } return dep[a]<dep[b]?a:b; } int chain[M],id; int stk[M],Top; bool mark[M]; int Kind[M]; void redfs(int now,int kind){ Kind[now]=kind; for(int i=head[now];i;i=edge[i].nx){ int nxt=edge[i].to; if(nxt==fa[now]||mark[nxt])continue; fa[nxt]=now; redfs(nxt,kind); } } int pre[M],aft[M]; void solve(int n,int m){ int a=Q[1].st,b=Q[1].ed,x=Q[1].lca; while(a!=x){//把這條路徑拉出來 mark[a]=1; chain[++id]=a; a=fa[a]; } chain[++id]=x; mark[x]=1; while(b!=x){ mark[b]=1; stk[++Top]=b; b=fa[b]; } while(Top)chain[++id]=stk[Top--]; int res=-1; for(int i=1;i<=id;i++){//對於每一個點染色 fa[chain[i]]=-1; redfs(chain[i],i); } for(int i=2;i<=m;i++){ int from=Kind[Q[i].st],to=Kind[Q[i].ed]; if(from==to){//這條路徑與最長路徑不相交 res=max(res,Q[i].d); continue; } if(from>to)swap(from,to);//保證from<to pre[to]=max(pre[to],Q[i].d); aft[from]=max(aft[from],Q[i].d); } for(int i=1;i<=id;i++)pre[i]=max(pre[i],pre[i-1]);//處理字首 for(int i=id;i>=1;i--)aft[i]=max(aft[i],aft[i+1]);//處理字尾 int ans=Q[1].d;//初始值,否則碰到m==1&&只有一條起點和終點重合的任務會掛 for(int i=1;i<id;i++){ int D=dis[chain[i]]-dis[chain[i+1]];//D為這條路徑的長度 if(D<0)D=-D; ans=min(ans,max4(pre[i],aft[i+1],Q[1].d-D,res)); } printf("%d\n",ans); } void Init(){ dfs(1); top[1]=1; dfs_top(1); } int main(){ int n,m; scanf("%d%d",&n,&m); for(int i=1;i<n;i++){ int a,b,d; scanf("%d%d%d",&a,&b,&d); Addedge(a,b,d); Addedge(b,a,d); } Init(); int mxid=0,mxd=0; for(int i=1;i<=m;i++){ scanf("%d%d",&Q[i].st,&Q[i].ed); Q[i].lca=LCA(Q[i].st,Q[i].ed); Q[i].d=dis[Q[i].st]+dis[Q[i].ed]-2*dis[Q[i].lca]; if(mxd<Q[i].d)mxd=Q[i].d,mxid=i; } swap(Q[mxid],Q[1]);//把最長邊與第一條邊交換一下。 solve(n,m); return 0; }