NOIP2015 D2T3 運輸計劃
拿到題目的第一眼
首先這是一棵n個節點的樹(別說你看不出來)
然後對於樹上的m條鏈
我們可以選取樹上的唯一一條邊使它的邊權變為0
求處理後最長鏈的長度
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函數換成樹上差分
最後發現正解只要一句話:
求鏈長+二分
上代碼:
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;
const int maxn=3e5+10;
struct edge{
int next,to,dis;
}e[2*maxn],q[2*maxn];
struct length{
int len,lca,u,v;
}len[maxn];
int head[maxn],cnt,headq[maxn],dis[maxn],maxlen,n,m,a[maxn],ans,s[maxn],num,ret,f[maxn]; //一堆變量
bool vis[maxn];
inline int readn() //隨處可見的快讀
{
int x=0;
char ch=getchar();
while(ch>‘9‘||ch<‘0‘)
ch=getchar();
while(ch>=‘0‘&&ch<=‘9‘)
{
x=(x<<3)+(x<<1)+(ch^‘0‘);
ch=getchar();
}
return x;
}
inline void add_edge(int x,int y,int d)
{
e[++cnt].next=head[x];
e[cnt].to=y;
e[cnt].dis=d;
head[x]=cnt;
}
inline void add_que(int x,int y)
{
q[++cnt].next=headq[x];
q[cnt].to=y;
headq[x]=cnt;
}
int find(int x)
{
return f[x]==x?f[x]:f[x]=find(f[x]); //一行並查集
}
void dfs(int u,int pre)
{
for(int i=head[u];i;i=e[i].next)
{
int v=e[i].to;
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;
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) //如果不能滿足,返回NO
return 0;
return 1; //能滿足
}
void tarjan(int u,int pre) //tarjan求鏈長
{
for(int i=head[u];i;i=e[i].next)
{
int v=e[i].to;
if(v==pre)
continue;
dis[v]=dis[u]+e[i].dis;
tarjan(v,u);
a[v]=e[i].dis;
int f1=find(v);
int f2=find(u);
if(f1!=f2)
f[f1]=find(f2);
vis[v]=1;
}
for(int i=headq[u];i;i=q[i].next)
if(vis[q[i].to])
{
int p=(i+1)>>1;
len[p].lca=find(q[i].to);
len[p].len=dis[u]+dis[q[i].to]-2*dis[len[p].lca];
maxlen=max(maxlen,len[p].len);
}
}
int main()
{
n=readn(),m=readn();
for(int i=1;i<n;i++)
{
int ai=readn(),bi=readn(),ti=readn();
add_edge(ai,bi,ti); //鄰接表存圖
add_edge(bi,ai,ti);
}
for(int i=1;i<=n;i++)
f[i]=i;
cnt=0;
for(int i=1;i<=m;i++)
{
int x=readn(),y=readn(); //輸入
len[i].u=x;
len[i].v=y;
add_que(x,y);
add_que(y,x);
}
tarjan(1,0);
int l=0,r=maxlen,mid;
while(l<=r) //二分答案
{
mid=(l+r)>>1;
if(check(mid))
{
r=mid-1;
ans=mid; //記錄答案
}
else
l=mid+1; //不能滿足則二分更大答案,以使得條件可以得到滿足
}
printf("%d\n",ans);
return 0;
}
NOIP2015 D2T3 運輸計劃