新&重&难点:
- 线段树建模,维护复杂信息;线段树的扫描线算法,平面几何计算问题
- 动态规划的二进制优化、单调队列优化
- 背包问题
- 倍增LCA求树上两点之间的距离;树上差分法;边的覆盖
区间动态规划
团子大家族
输入格式:
第一行一个字符串
输出格式:
一行一个整数表示答案
输入样例:
Ab3bd
输出样例:
2
仔细读题,可以将问题转化为求最长回文子串,用总长度减去最长回文子串的长度,得到剩余的字符个数,只要往原串种相应地加入这些剩余的字符,就能构成回文串。
注意到有求回文串的情况,考虑用区间DP,设
f
(
l
,
r
)
f(l,r)
f(l,r)表示区间
[
l
,
r
]
[l,r]
[l,r]里最长回文子串的长度。由于大区间合并十分不方便且并不能保证正确性,如合并aaaa
和bbbb
二者均为回文串,但合并起来就不是回文串了。
考虑逐步扩展的情况发现:
- 扩展的字符不算进回文子串里,对于任意确定区间 [ l , r ] [l,r] [l,r],可能由 [ l + 1 , r ] [l+1,r] [l+1,r]或 [ l , r − 1 ] [l,r-1] [l,r−1]转移而来: f ( l , r ) = max { f ( l + 1 , r ) , f ( l , r − 1 ) } f(l,r)=\max\{f(l+1,r),f(l,r-1)\} f(l,r)=max{f(l+1,r),f(l,r−1)}
- 如果使扩展的字符加入回文子串,显然有条件 S l = S r S_l=S_r Sl=Sr所以得到转移: f ( l , r ) = f ( l + 1 , r − 1 ) + 2 , S l = S r f(l,r) = f(l+1,r-1)+2,\space \space \space S_l = S_r f(l,r)=f(l+1,r−1)+2, Sl=Sr
只要对以上两种情况分别讨论且求最大值即可 O ( n 2 ) O(n^2) O(n2)
#include<iostream>
#include<string>
#include<cstring>
using namespace std;
const int inf = 0xfffffff;
int n,f[1005][1005],a[1005];
string s;
int main()
{
cin >> s;
for(int i = 0;i < s.size();i ++) a[i+1] = s[i];
n = s.size();
for(int i = 1;i <= n;i ++)
f[i][i] = 1;
for(int len = 2;len <= n;len ++)
{
for(int l = 1;l <= n-len+1;l ++)
{
int r = l + len - 1;
if(a[l] == a[r]) f[l][r] = max(f[l][r],f[l+1][r-1]+2);
else f[l][r] = max(f[l][r],max(f[l+1][r],f[l][r-1]));
}
}
cout << n-f[1][n];
return 0;
}
然而题解说:
于是我们得到一种新的求最长回文子串的方法。证明的话,由于回文串从前往后读或从后往前读都是一样的,而对于其他字符组成的子序列就不具备这个性质,利用这个性质,其他字符组成的子序列反转后并不能与原子序列完全匹配。
从上图可知:对于不具备回文性质的子串,最长公共子序列长度为1,而对于回文性质的子串,最长公共子序列长度为它本身的长度。
所以原字符串和其反转后的字符串的最长公共子序列一定是最长回文子串。
O
(
n
2
)
O(n^2)
O(n2)从常数来看,显然区间DP更优
P3205 [HNOI2010]合唱队
P3205 [HNOI2010]合唱队
本质:钦定一个插入队列的顺序,使得最后的队伍是理想队伍,求这种顺序的方案数
题目中隐藏的重要性质:
- 对于当前队列 [ l , r ] [l,r] [l,r],最后一个插入的人一定是 l l l或 r r r
所以对于当前队列
[
l
,
r
]
[l,r]
[l,r],插入一个人
i
i
i,可以视作对区间的扩展操作:
i
i
i一定是
l
−
1
l\!-\!1
l−1或
r
+
1
r\!+\!1
r+1,因为只有这样才能使最后排得的队形是理想队形。
设
f
(
l
,
r
,
p
o
s
)
f(l,r,pos)
f(l,r,pos)表示排好当前区间
[
l
,
r
]
[l,r]
[l,r]且最后一个插入的人是
p
o
s
pos
pos(
p
o
s
pos
pos只能是
l
l
l或
r
r
r)的方案数,那么有:
f
(
l
,
r
,
l
)
=
[
h
(
l
)
<
h
(
l
+
1
)
]
×
f
(
l
+
1
,
r
,
l
+
1
)
+
[
h
(
l
)
<
h
(
r
)
]
×
f
(
l
+
1
,
r
,
r
)
f
(
l
,
r
,
r
)
=
[
h
(
r
)
>
h
(
l
)
]
×
f
(
l
,
r
−
1
,
l
)
+
[
h
(
r
)
>
h
(
r
−
1
)
]
×
f
(
l
,
r
−
1
,
r
−
1
)
f(l,r,l) =[h(l)<h(l\!+\!1)]\times f(l\!+\!1,r,l\!+\!1)+[h(l)<h(r)]\times f(l\!+\!1,r,r)\\ f(l,r,r) =[h(r)>h(l)]\times f(l,r\!-\!1,l)+[h(r)>h(r\!-\!1)]\times f(l,r\!-\!1,r\!-\!1)
f(l,r,l)=[h(l)<h(l+1)]×f(l+1,r,l+1)+[h(l)<h(r)]×f(l+1,r,r)f(l,r,r)=[h(r)>h(l)]×f(l,r−1,l)+[h(r)>h(r−1)]×f(l,r−1,r−1)
其中,函数
[
p
]
[p]
[p]表示如果命题
p
p
p成立则
[
p
]
=
1
[p]=1
[p]=1,否则
[
p
]
=
0
[p]=0
[p]=0;函数
h
(
i
)
h(i)
h(i)表示第
i
i
i个人的身高
最终答案就是
f
(
1
,
n
,
1
)
+
f
(
1
,
n
,
n
)
f(1,n,1)+f(1,n,n)
f(1,n,1)+f(1,n,n)
最后要注意计算区间长度等于
2
2
2的
f
f
f时的一些小细节,不要重复计数
#include<iostream>
#include<cstdio>
using namespace std;
const long long modn = 19650827;
int n,a[1005];
long long f[1005][1005][2];
int main()
{
scanf("%d",&n);
for(int i = 1;i <= n;i ++)
{
scanf("%d",&a[i]);
f[i][i][0] = f[i][i][1] = 1;
}
for(int len = 2;len <= n;len ++)
{
for(int i = 1;i <= n-len+1;i ++)
{
int j = i + len - 1;
if(len == 2)
{
if(a[i] < a[i+1]) f[i][j][0] += f[i+1][j][0]%modn;
f[i][j][0] %= modn;
if(a[j] > a[i]) f[i][j][1] += f[i][j-1][0]%modn;
f[i][j][1] %= modn;
}
else
{
if(a[i] < a[i+1]) f[i][j][0] += f[i+1][j][0]%modn;
f[i][j][0] %= modn;
if(a[i] < a[j]) f[i][j][0] += f[i+1][j][1]%modn;
f[i][j][0] %= modn;
if(a[j] > a[i]) f[i][j][1] += f[i][j-1][0]%modn;
f[i][j][1] %= modn;
if(a[j] > a[j-1]) f[i][j][1] += f[i][j-1][1]%modn;
f[i][j][1] %= modn;
}
}
}
// printf("%lld\n",f[1][n][0]);
// printf("%lld\n",f[1][n][1]);
printf("%lld",(f[1][n][0]%modn+f[1][n][1]%modn)%modn);
return 0;
}
子序列
由狄尔沃斯定理得
最长不上升子序列的个数等于最长上升子序列的长度
P1439 【模板】最长公共子序列
/*
LCS DP
设f[i][j]表示匹配到P1的第i位,P2的第j位的最长公共子序列长度
f[i][j] = max(f[i][j],f[i-1][j],f[i][j-1])
if(p1[i] == p2[j]) f[i][j] = max(f[i][j],f[i-1][j-1]+1);
两个排列的LCS由S组成,S中的元素满足i < j,k < k',p1[i] == p2[k],p1[j] == p2[k']
即这些元素在两个排列中的位置都要单调递增
在枚举期间已经保证元素在P1中位置单调递增,只要保证P1中的元素在P2中位置单调递增即可
由两个排列的LCS 转为 P1中的元素在P2中位置的LIS最长上升子序列
二分优化LIS求法,O(nlogn);
*/
#include<iostream>
#include<algorithm>
#include<cstdio>
using namespace std;
const int inf = 0xfffffff;
int n,a[100005],b[100005],pos[100005],pos2[100005];
int f[100005],ans,len,l,r,mid;
int main()
{
scanf("%d",&n);
for(int i = 1; i <= n;i ++)
{
scanf("%d",&a[i]);
pos2[a[i]] = i;
}
for(int i = 1; i <= n;i ++)
{
scanf("%d",&b[i]);
pos[pos2[b[i]]] = i;
}
// for(int i = 1; i <= n;i ++)//LCS DP
// {
// for(int j = 1;j <= n;j ++)
// {
// if(a[i] == b[j]) f[i][j] = max(f[i][j],f[i-1][j-1]+1);
// f[i][j] = max(f[i][j],max(f[i-1][j],max(f[i-1][j-1],f[i][j-1])));
// ans = max(f[i][j],ans);
// }
// }
for(int i = 1;i <= n;i ++)
{
if(pos[i] > f[ans])
{
ans ++;
f[ans] = pos[i];
}
else
{
l = 1;r = ans;mid = 0;
while(l <= r)
{
mid = (l+r)/2;
if(f[mid] > pos[i]) r = mid - 1;
else l = mid + 1;
}
f[l] = min(pos[i],f[l]);
}
}
cout << ans;
return 0;
}
#include<iostream>
#include<cstdio>
using namespace std;
int n,a[5005],f[5005],ans,f2[5005];
int main()
{
scanf("%d",&n);
for(int i = 1;i <= n;i ++) scanf("%d",&a[i]);
f[1] = 1;f2[1] = 1;
for(int i = 1;i <= n+1;i ++)/*以n+1作为结尾,f2[n+1]统计了总方案数*/
{
for(int j = i - 1;j >= 1;j --)//暴力n^2DP
if(a[j] > a[i])
{
if(f[j] + 1 > f[i])
{
f[i] = f[j] + 1;//最长下降子序列
f2[i] = f2[j];//更新方案数
}
else if(f[j] + 1 == f[i]) f2[i] += f2[j];//累加方案数
}
f[i] = max(f[i],1);
if(f[i] == 1) f2[i] = 1;
for(int j = 1;j < i;j ++)
if(f[i] == f[j] && a[i] == a[j])//去除先前重复的计数
f2[j] = 0;
ans = max(ans,f[i]);
}
printf("%d %d",ans-1,f2[n+1]);//减去a[n+1]这支假股票
return 0;
}
背包
P2340 [USACO03FALL]Cow Exhibition G
P2340 [USACO03FALL]Cow Exhibition G
背包问题果真不熟练
- 对于每头奶牛只有两种决策:选和不选,考虑01背包
- 考虑将两个量情商和智商用01背包中的体积和价值表示,设 f ( k , i ) f(k,i) f(k,i) 表示在前 k k k 个奶牛中选择时,情商为 i i i 时的智商最大值,这样显然容易判断情商智商二者是否为负数。显然有: f ( k , i ) = max { f ( k , i ) , f ( k − 1. i − E Q ( k ) ) + I Q ( k ) } f(k,i) = \max\{f(k,i),f(k-1.i-EQ(k))+IQ(k)\} f(k,i)=max{f(k,i),f(k−1.i−EQ(k))+IQ(k)}
- 小细节:注意到数组不能存在负数下标,根据数据范围拟定一个偏移量,设 0 = 400000 0=400000 0=400000即可
- 省去一维的时候,决策时应考虑 E Q ( k ) EQ(k) EQ(k)的正负性,如果为非负,那么正常倒序 DP \text{DP} DP即可,否则应正序 DP \text{DP} DP,防止重复计算
#include<iostream>
#include<cstdio>
using namespace std;
const int inf = 0xfffffff;
const int zero = 400000;
int n,a[505],b[505],ans;
int f[800005];
int main()
{
scanf("%d",&n);
for(int i = 1;i <= n;i ++)
scanf("%d%d",&a[i],&b[i]);
for(int i = 0;i <= 800000;i ++) f[i] = -inf;
f[zero] = 0;
for(int i = 1;i <= n;i ++)
{
if(a[i] >= 0)
{
for(int j = 800000;j >= 0 && j >= a[i];j --)
f[j] = max(f[j],f[j-a[i]] + b[i]);
}
else
{
for(int j = 0;j <= 800000+a[i];j ++)
f[j] = max(f[j],f[j-a[i]] + b[i]);
}
}
for(int j = zero;j <= 800000;j ++)
if(f[j] >= 0)
ans = max(ans,f[j]+j-zero);
printf("%d",ans);
return 0;
}
小结:
01背包的特征:两种决策方式:选与不选
巧用题目中的条件限制,依据条件之间的关系,将它们转化为01背包的两大要素:体积和价值
P1156 垃圾陷阱
P1156 垃圾陷阱
根据先前做题经验的指导,巧妙将当前的高度视作体积,将存活最长时间视作价值,20分钟写出状态转移方程。但最后因为调小细节花了一个小时。。。
#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
const int inf = 0xfffffff;
int di,n,f[3005][305];
int ans2 = 10;
struct rub{
int ti;
int hi;
int fi;
}a[505];
bool cmp(rub x,rub y)
{
return x.ti < y.ti ;
}
int main()
{
scanf("%d%d",&di,&n);
for(int i = 1;i <= n;i ++)
scanf("%d%d%d",&a[i].ti,&a[i].fi,&a[i].hi);
sort(a+1,a+1+n,cmp);
for(int i = 0;i <= n;i ++)
for(int j = 0;j <= di;j ++)
f[i][j] = -1;
f[1][0] = 10;
if(a[1].ti <= 10)
{
for(int i = 1;i <= n;i ++)
{
for(int j = 0;j <= di;j ++)
{
if(f[i][j] >= a[i].ti)
{
f[i+1][j+a[i].hi] = max(f[i+1][j+a[i].hi],f[i][j]);
if(j+a[i].hi >= di)
{
printf("%d",a[i].ti);
return 0;
}
ans2 = max(ans2,f[i][j]);
f[i+1][j] = max(f[i+1][j],f[i][j] + a[i].fi);
ans2 = max(ans2,f[i][j]+a[i].fi);
}
}
}
}
printf("%d\n",ans2);
return 0;
}
二进制优化
/*
二进制优化
*/
#include<iostream>
#include<cstdio>
using namespace std;
const int inf = 0xfffffff;
typedef long long ll;
int n,W,v[105],w[105],m[105];
ll ans,f[40005],maxn;
int main()
{
scanf("%d%d",&n,&W);
for(int i = 1;i <= n;i ++) scanf("%d%d%d",&v[i],&w[i],&m[i]);
for(int i = 0;i <= W;i ++) f[i] = -inf;
for(int i = 0;i <= m[1] && w[1]*i <= W;i ++)
f[w[1]*i] = v[1]*i;
for(int i = 2;i <= n;i ++)
{
maxn = min(m[i],W/w[i]);
for(int j = 1;maxn > 0;j <<= 1)
{
if(j > maxn) j = maxn;
maxn -= j;
for(int k = W;k >= w[i]*j;k --)
{
f[k] = max(f[k],f[k-j*w[i]]+j*v[i]);
ans = max(ans,f[k]);
}
}
}
printf("%lld",ans);
return 0;
}
LCA
树边覆盖问题:树上差分算法
#10131. 「一本通 4.4 例 2」暗的连锁
/*
如果第一步切断x,y之间的便,那么第二步必须切断附加边(x,y),才能令其分成不连通的两部分
我们称每条附加边(x,y)把树上的x,y之间的路径的每条边都覆盖了一遍
若第一步切断覆盖了0次的主要边,则第二步可以任意切断一条附加边
若第一步切断覆盖了1次的主要边,则第二步只能切断对应的附加边
若第一步切断覆盖2次或以上的主要边,则第二步无论怎么切也不能将其斩成不连通的两部分
树上差分算法:
对于每条非树边(x,y)令结点x和y的权值+1,LCA(x,y)的权值-2
最后DFS一遍,求出F(x)表示以x为根的子树中各个结点的权值和
F(x)就是x与它的父结点之间的树边的被覆盖次数
O(n+m)
*/
#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
typedef long long ll;
int n,m,a[300005],f[100005][25],x,y,sum[300005],dep[300005];
int head[300005],cnt,lca;
ll ans;
struct edge{
int nxt;
int to;
}e[300005];
void addedge(int from,int to)
{
cnt ++;
e[cnt].nxt = head[from];
e[cnt].to = to;
head[from] = cnt;
return ;
}
void prework(int u,int fa)
{
dep[u] = dep[fa] + 1;
for(int i = 0;i < 19;i ++) f[u][i+1] = f[f[u][i]][i];
for(int i = head[u];i;i = e[i].nxt)
{
int v = e[i].to;
if(v == fa) continue;
f[v][0] = u;
prework(v,u);
}
return ;
}
int GetLCA(int x,int y)
{
if(dep[x] < dep[y]) swap(x,y);
for(int i = 20; i >= 0; i --)
{
if(dep[f[x][i]] >= dep[y]) x = f[x][i];
if(x == y) return x;
}
for(int i = 20;i >= 0;i --)
{
if(f[x][i] != f[y][i])
{
x = f[x][i];
y = f[y][i];
}
}
return f[x][0];
}
void dfs(int u)
{
for(int i = head[u];i;i = e[i].nxt)
{
int v = e[i].to;
if(v != f[u][0])
{
dfs(v);
sum[u] += sum[v];
}
}
sum[u] += a[u];
return ;
}
int main()
{
scanf("%d%d",&n,&m);
for(int i = 1;i < n;i ++)
{
scanf("%d%d",&x,&y);
addedge(x,y);
addedge(y,x);
}
prework(1,0);
for(int i = 1;i <= m;i ++)//树上差分算法
{
scanf("%d%d",&x,&y);
a[x] ++;a[y] ++;
lca = GetLCA(x,y);
a[lca] -= 2;
}
dfs(1);//统计F(x)
for(int i = 2;i <= n;i ++)//统计最后的答案
{
if(sum[i] == 0) ans += m;
if(sum[i] == 1) ans ++;
}
printf("%lld",ans);
return 0;
}
P6869 [COCI2019-2020#5] Putovanje
P6869 [COCI2019-2020#5] Putovanje
初步想法:统计每条边被经过的次数,然后计算最小费用
朴素算法:枚举所有节点
O
(
n
)
O(n)
O(n) ,求LCA
O
(
log
n
)
O(\log n)
O(logn) ,然后从起始节点一步一步跳到LCA统计每条边
O
(
n
)
O(n)
O(n) ,时间复杂度大概是
O
(
n
2
log
n
)
O(n^2\log n)
O(n2logn)
对于
n
≤
200000
n\le 200000
n≤200000 的数据,朴素算法超时
考虑到 统计每条边被经过的次数
⇔
\Leftrightarrow
⇔求每个结点与其父亲结点相连的边的覆盖次数
所以使用树上差分的算法
对于每条非树边(x,y)令结点x和y的权值+1,LCA(x,y)的权值-2
最后DFS一遍,求出F(x)表示以x为根的子树中各个结点的权值和
F(x)就是x与它的父结点之间的树边的被覆盖次数
从而时间复杂度降至
O
(
n
log
n
)
O(n\log n)
O(nlogn)
#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
int n,fa[200005][25],dep[200005],upedge[200005];
int head[200005],cnt,lca,x,y;
long long f[200005],cnter[200005],w,z,cov[200005];
struct edge{
int nxt;
int to;
long long c1;
long long c2;
}e[500005];
void addedge(int from,int to,long long cost1,long long cost2)
{
cnt ++;
e[cnt].nxt = head[from];
e[cnt].to = to;
e[cnt].c1 = cost1;
e[cnt].c2 = cost2;
head[from] = cnt;
return ;
}
void dfs(int u,int father)
{
dep[u] = dep[father] + 1;
for(int i = 1;i <= 20;i ++) fa[u][i] = fa[fa[u][i-1]][i-1];
int v = 0;
for(int i = head[u];i;i = e[i].nxt)
{
v = e[i].to;
if(v == father)
{
upedge[u] = i;
continue;
}
fa[v][0] = u;
dfs(v,u);
}
return ;
}
int GetLCA(int x,int y)
{
if(dep[x] < dep[y]) swap(x,y);
for(int i = 20;i >= 0;i --)
{
if(dep[fa[x][i]] >= dep[y]) x = fa[x][i];
if(x == y) return x;
}
for(int i = 20;i >= 0;i --)
{
if(fa[x][i] != fa[y][i])
{
x = fa[x][i];
y = fa[y][i];
}
}
return fa[x][0];
}
void dfs2(int u)
{
int v = 0;
for(int i = head[u];i;i = e[i].nxt)
{
v = e[i].to;
if(v == fa[u][0]) continue;
dfs2(v);cov[u] += cov[v];
}
cov[u] += cnter[u];
return ;
}
int main()
{
scanf("%d",&n);
for(int i = 1;i < n;i ++)
{
scanf("%d%d%lld%lld",&x,&y,&z,&w);
addedge(x,y,z,w);
addedge(y,x,z,w);
}
dfs(1,0);
for(int i = 2;i <= n;i ++)
{
lca = GetLCA(i-1,i);
cnter[i-1] ++;cnter[i] ++;cnter[lca] -= 2;//树上差分
//不赋值,做运算,否则会出错
}
dfs2(1);
for(int i = 2;i <= n;i ++)
f[i] = f[i-1]+min(cov[i]*e[upedge[i]].c1,e[upedge[i]].c2);
printf("%lld",f[n]);
return 0;
}
AHOI2008紧急集合 / 聚会
/*仔细思考发现题目本质
1.求使得指定的3个点联通的边集总长度最小值
2.求一个结点使得所有指定结点到这个结点的路径都没有重叠
Q1:发现总长度最小值 = 两两结点之间距离/2
Q2:发现在两两结点组成的3个LCA中,深度最深的LCA使得路径不会重叠
*/
#include<iostream>
#include<cstdio>
using namespace std;
int n,a,b,m,x,y,z,ans,dep[500005];
int head[500005],cnt,f[500005][25],lca1,lca2,lca3,dis1,dis2,dis3,mindis;
struct edge{
int nxt;
int to;
}e[1000005];
void addedge(int from,int to)
{
cnt ++;
e[cnt].nxt = head[from];
e[cnt].to = to;
head[from] = cnt;
return ;
}
void prework(int u,int fa)
{
dep[u] = dep[fa] + 1;
for(int i = 0;i < 20;i ++) f[u][i+1] = f[f[u][i]][i];
for(int i = head[u];i;i = e[i].nxt)
{
int v = e[i].to;
if(v == fa) continue;
f[v][0] = u;
prework(v,u);
}
return ;
}
int GetLCA(int x,int y)
{
if(dep[x] < dep[y]) swap(x,y);
for(int i = 20;i >= 0;i --)
{
if(dep[f[x][i]] >= dep[y]) x = f[x][i];
if(x == y) return x;
}
for(int i = 20;i >= 0;i --)
{
if(f[x][i] != f[y][i])
{
x = f[x][i];
y = f[y][i];
}
}
return f[x][0];
}
int main()
{
scanf("%d%d",&n,&m);
for(int i = 1;i < n;i ++)
{
scanf("%d%d",&a,&b);
addedge(a,b);
addedge(b,a);
}
prework(1,0);
for(int i = 1;i <= m;i ++)
{
scanf("%d%d%d",&x,&y,&z);
lca1 = GetLCA(x,y);
lca2 = GetLCA(y,z);
lca3 = GetLCA(x,z);
mindis = dep[x] + dep[y] + dep[z] - dep[lca1] - dep[lca2] - dep[lca3];
if(dep[lca1] >= dep[lca2] && dep[lca1] >= dep[lca3]) ans = lca1;
if(dep[lca2] >= dep[lca1] && dep[lca2] >= dep[lca3]) ans = lca2;
if(dep[lca3] >= dep[lca1] && dep[lca3] >= dep[lca2]) ans = lca3;
printf("%d %d\n",ans,mindis);
}
return 0;
}
倍增推广、树形dp和LCA综合使用
11.6
异闻带
输入样例:
5 2 4
3 2
1 9
1 2
2 6
4 5
2 5
2 3
2 2
4 5
输出样例:
11
Yep
0
Nop
用树上倍增法暴力跳跃,求深度和祖先的同时,可以求出每个点到根节点的距离,然后对于给定的两个点,求它们的LCA判断能否跳到1,如果
y
y
y在
x
x
x的子树里,暴力枚举特殊点求最短距离可以得到50分的好成绩
O
(
q
m
log
n
)
O(qm\log n)
O(qmlogn)
对于进一步的优化,可以考虑在预处理的同时求出每个结点
i
i
i到它子树下某一特殊点的最短距离,然后在求最短距离的时候该暴力枚举为倍增枚举可到达特殊点的祖先,在这些祖先中求最短距离,成功将复杂度降至
O
(
q
log
n
)
O(q\log n)
O(qlogn)
#include<iostream>
#include<cstdio>
using namespace std;
const long long inf = 99999999999999;
typedef long long ll;
bool sp[100005];
int n,m,q,x,fa[100005],y,lca;
int f[100005][30],dep[100005];
ll ans,d[100005],d2[100005];
int head[100005],cnt;
struct edge{
int nxt;
int to;
ll val;
}e[400005];
void addedge(int from,int to,ll dis)
{
cnt ++;
e[cnt].nxt = head[from];
e[cnt].to = to;
e[cnt].val = dis;
head[from] = cnt;
return ;
}
void prework(int u,int father)
{
dep[u] = dep[father] + 1;//记录深度
if(sp[u] == 1) d2[u] = 0;//如果当前就是特殊点,那么自己到自己的距离为0
for(int i = 0;i < 20;i ++) f[u][i+1] = f[f[u][i]][i];//倍增求祖先
int v = 0,w = 0;
for(int i = head[u];i;i = e[i].nxt)
{
v = e[i].to;w = e[i].val;
f[v][0] = u;
d[v] = d[u] + w;//第i个点到根节点的距离
prework(v,u);
d2[u] = min(d2[u],d2[v]+w);//更新i到子树中任一特殊点的最短距离
}
return ;
}
int GetLCA(int u,int v)
{
if(dep[u] < dep[v]) swap(u,v);
for(int i = 20;i >= 0;i --)
{
if(dep[f[u][i]] >= dep[v]) u = f[u][i];
if(u == v) return u;
}
for(int i = 20;i >= 0;i --)
{
if(f[u][i] != f[v][i])
{
u = f[u][i];
v = f[v][i];
}
}
return f[u][0];
}
int main()
{
scanf("%d%d%d",&n,&m,&q);
for(int i = 1;i <= m;i ++)
{
scanf("%d",&x);
sp[x] = 1;//记录特殊点
}
d2[1] = inf;//初始化,记录以i为根的子树中特殊点到i的最短距离
for(int i = 2;i <= n;i ++)
{
scanf("%d%lld",&fa[i],&y);
addedge(fa[i],i,y);//建树
d2[i] = inf;
}
prework(1,0);//处理深度等
for(int i = 1;i <= q;i ++)
{
scanf("%d%d",&x,&y);
lca = GetLCA(x,y);//求两者的最近公共祖先
if(lca != x || lca == 1) printf("Yep\n");//最近公共祖先不是x,说明y不在以x为根的子树下,不会受到f(x)-x这条断边的影响
else
{
ans = inf;int tmp = y;
for(int j = 20;j >= 0 ;j --)
if(d2[f[tmp][j]] == inf && dep[f[tmp][j]] >= dep[x])
tmp = f[tmp][j];//倍增求y的最近可到达特殊点的祖先
for(;dep[tmp]>=dep[x];tmp = f[tmp][0])
if(d2[tmp] != inf)
ans = min(ans,d2[tmp] - d[tmp] + d[y]); //求最短距离
if(ans == inf) printf("Nop\n");//到断层的所有的y的祖先都不能到达特殊点
else printf("%lld\n",ans);
}
}
return 0;
}
/*
input:
8 2 5
5 8
1 2
1 3
2 1
2 2
4 1
4 4
4 8
2 2
2 6
4 5
14 4 5
output:5
Yep
Yep
19
6
input:
7 9 11 2
1 5
1 3
2 9
2 8
2 6
3 6
3 2
7 7
7 5
8 1
8 13
12 5
12 8
7 10
5 14
12 8
8 13
2 6
output:
2
4
Yep
*/
线段树优化
减少无用的修改
花神游历各国
#include<iostream>
#include<cstdio>
#include<cmath>
using namespace std;
typedef long long ll;
ll n,a[100005],q,x,y,z;
struct SegTree{
ll maxn;
ll sum;
}t[400005];
void Build(ll k,ll l,ll r)
{
if(l == r)
{
t[k].maxn = t[k].sum = a[l];
return ;
}
ll mid = (l+r)/2;
ll lc = (k<<1);ll rc = (k<<1)+1;
Build(lc,l,mid);
Build(rc,mid+1,r);
t[k].sum = t[lc].sum + t[rc].sum;
t[k].maxn = max(t[lc].maxn,t[rc].maxn);
return ;
}
ll Query(ll k,ll l,ll r,ll x,ll y)
{
if(l > y || r < x) return 0;
if(x <= l && r <= y) return t[k].sum;
ll mid = (l+r)/2;
ll lc = k << 1;ll rc = (k<<1)+1;
ll ltmp = Query(lc,l,mid,x,y);
ll rtmp = Query(rc,mid+1,r,x,y);
return ltmp+rtmp;
}
void Modify(ll k,ll l,ll r,ll x,ll y)
{
if(l > y || r < x) return ;
if(l == r && x <= l && r <= y)
{
t[k].maxn = t[k].sum = floor(sqrt(t[k].sum));
return ;
}
if(t[k].maxn < 2) return ;//线段树剪枝。。。减少无意义的修改
ll mid = (l+r)/2;
ll lc = k << 1;ll rc = (k<<1)+1;
Modify(lc,l,mid,x,y);
Modify(rc,mid+1,r,x,y);
t[k].maxn = max(t[lc].maxn,t[rc].maxn);
t[k].sum = t[lc].sum + t[rc].sum;
return ;
}
int main()
{
scanf("%lld",&n);
for(ll i = 1;i <= n;i ++)
scanf("%lld",&a[i]);
Build(1,1,n);
scanf("%lld",&q);
for(ll i = 1;i <= q;i ++)
{
scanf("%lld",&z);scanf("%lld%lld",&x,&y);
if(z == 1)
printf("%lld\n",Query(1,1,n,x,y));
else
Modify(1,1,n,x,y);
}
return 0;
}
状态压缩优化信息储存
P5522 [yLOI2019] 棠梨煎雪
P5522 [yLOI2019] 棠梨煎雪
对要维护的信息进行状态压缩
#include<iostream>
#include<cstdio>
#include<string>
#include<cstring>
#include<fstream>
using namespace std;
typedef unsigned long long ull;
int n,m,q,x,y,opt;
ull ans,sum;
string str[100010],newstr;
struct state{
int st0;
int st1;
int st2;
}tr[500007],rep;
void compr(int k,string s)
{
tr[k].st0 = tr[k].st1 = tr[k].st2 = 0;
for(int i = 0;i < m;i ++)
{
if(s[i] == '0') tr[k].st0 |= (1<<i);
if(s[i] == '1') tr[k].st1 |= (1<<i);
if(s[i] == '?') tr[k].st2 |= (1<<i);
}
return ;
}
state mergest(state s1,state s2)
{
state res;
res.st0 = (s1.st0|s2.st0);
res.st1 = (s1.st1|s2.st1);
res.st2 = (s1.st2|s2.st2);
return res;
}
ull cntst(state sta)
{
if((sta.st0&sta.st1) > 0) return 0;
int sta2 = ((sta.st2-(sta.st0&sta.st2))&(sta.st2-(sta.st1&sta.st2)));
ull res = 1;
while(sta2)
{
res <<= 1;
sta2 = sta2&(sta2-1);
}
return res;
}
void Build(int k,int l,int r)
{
if(l == r)
{
compr(k,str[l]);
return ;
}
int mid = ((l+r)>>1);
int lson = (k<<1);int rson = lson + 1;
Build(lson,l,mid);
Build(rson,mid+1,r);
tr[k] = mergest(tr[lson],tr[rson]);
return ;
}
void Modify(int k,int l,int r,int pos)
{
if(l > pos || r < pos) return ;
if(l == r && l == pos)
{
compr(k,newstr);
return ;
}
int mid = ((l+r)>>1);
int lson = (k<<1);int rson = lson + 1;
Modify(lson,l,mid,pos);
Modify(rson,mid+1,r,pos);
tr[k] = mergest(tr[lson],tr[rson]);
return ;
}
state Query(int k,int l,int r,int x,int y)
{
state res;
if(l > y || r < x)
{
res.st0 = res.st1 = res.st2 = 0;
return res;
}
if(x <= l && r <= y)
return tr[k];
int mid = ((l+r)>>1);
int lson = (k<<1);int rson = lson + 1;
state tmp1,tmp2;
tmp1 = Query(lson,l,mid,x,y);
tmp2 = Query(rson,mid+1,r,x,y);
res = mergest(tmp1,tmp2);
return res;
}
int main()
{
scanf("%d%d%d",&m,&n,&q);
for(int i = 1;i <= n;i ++)
cin >> str[i];
Build(1,1,n);
for(int i = 1;i <= q;i ++)
{
scanf("%d",&opt);
if(opt == 0)
{
scanf("%d%d",&x,&y);
rep = Query(1,1,n,x,y);
sum = cntst(rep);
ans = ans^sum;
}
else
{
scanf("%d",&x);
cin >> newstr;
Modify(1,1,n,x);
}
}
printf("%llu",ans);
return 0;
}
状态压缩动态规划
P3694 邦邦的大合唱站队
P3694 邦邦的大合唱站队
看到乐队的数量
M
≤
20
M\le 20
M≤20,考虑从此处寻找突破口
排好的队伍一定如下
111222333
222333111
333222111
…
设
S
S
S 表示队伍状态,某一位为
1
1
1 即某支乐队已经排好,对于一个状态
S
S
S 可以有多种情况,如一共有
4
4
4 支乐队,现状态
S
=
1100
S=1100
S=1100
则有1112223434或2221113434。
f
(
S
)
f(S)
f(S) 表示排成状态
S
S
S 的最小出列人数,显然当再排一支队伍的时候,我们不必管前面排好的队伍是什么顺序,因为无论如何,排好队伍的总人数是一定的,我们只要使新的将要排好的队伍接在早已经排好的队伍后面就好了
#include<iostream>
#include<cstdio>
using namespace std;
const int inf = 0xfffffff;
int n,m,tot,t[25],c[25],s1,ssum,add;
int sum[25][200005],a[200005],cnt[25];
int f[1048676],ans;
int main()
{
ans = inf;
scanf("%d%d",&n,&m);
tot = (1<<m) - 1;
for(int i = 1;i <= n;i ++)
{
scanf("%d",&a[i]);
for(int j = 1;j <= m;j ++) sum[j][i] = sum[j][i-1];
sum[a[i]][i] ++;
cnt[a[i]] ++;
}
for(int i = 1;i <= tot;i ++) f[i] = inf;
for(int s = 1;s <= tot;s ++)
{
t[0] = 0;ssum = 0;
for(int i = 1;i <= m;i ++)
{
if((s&(1<<(i-1))) > 0)
{
t[0] ++;
t[t[0]] = i;
ssum += cnt[i];
}
}
for(int i = 1;i <= t[0];i ++)
{
s1 = s - (1<<(t[i]-1));
int l = ssum - cnt[t[i]];
int r = ssum;
add = cnt[t[i]] - (sum[t[i]][r] - sum[t[i]][l]);
f[s] = min(f[s],f[s1] + add);
}
}
printf("%d",f[tot]);
return 0;
}
最短路
最优贸易
经典题:最优贸易
考虑以某一点
k
k
k 作为中转点。
从
1
1
1 到
k
k
k 的路途中,要在经过的某一城市买一个水晶球。
从
k
k
k 到
n
n
n 的路途中,要在经过的某一城市将刚刚买入的水晶球卖出。
显然可得:在买水晶球的路上,价格越低越好;在卖水晶球的路上,价格越高越好;
则对于选择
k
k
k 作为中转站,设
V
1
V_1
V1 表示从
1
1
1 到
k
k
k 的路程上可能经过的城市集合,
V
2
V_2
V2 表示从
k
k
k 到
n
n
n 的路程上可能经过的城市集合
其赚取最多的旅费
f
(
k
)
=
max
i
∈
V
2
{
c
o
s
t
(
i
)
}
−
min
j
∈
V
1
{
c
o
s
t
(
j
)
}
f(k)=\max_{i\in V_2}\{cost(i)\}-\min_{j\in V_1}\{cost(j)\}
f(k)=i∈V2max{cost(i)}−j∈V1min{cost(j)}
由此,最终的答案为
max
1
<
k
<
n
{
f
(
k
)
}
\max_{1<k<n}\{f(k)\}
max1<k<n{f(k)}
#include<iostream>
#include<cstring>
#include<string>
#include<cstdio>
#include<queue>
#include<fstream>
using namespace std;
const int inf = 0xfffffff;
int n,m,a[100005],ans;
int maxn[100005],minn[100005];
int x,y,z,cnt,head[100005];
bool vis[100005];
struct edge{
int nxt;
int to;
int tag;
}e[1500005];
void addedge(int from,int to,int mark)
{
cnt ++;
e[cnt].nxt = head[from];
e[cnt].to = to;
e[cnt].tag = mark;
head[from] = cnt;
return ;
}
void SPFA_neg()
{
int u = 0, v = 0;
queue<int > q;
memset(vis,0,sizeof(vis));
q.push(n);vis[n] = 1;maxn[n] = a[n];
while(!q.empty())
{
u = q.front();
q.pop();
vis[u] = 0;
for(int i = head[u];i;i = e[i].nxt)
if(e[i].tag == 1 || e[i].tag == 2)
{
v = e[i].to;
if(maxn[v] < maxn[u] || maxn[v] < a[v])
{
maxn[v] = max(maxn[u],a[v]);
if(!vis[v])
{
q.push(v);
vis[v] = 1;
}
}
}
}
}
void SPFA()
{
int u = 0, v = 0;
queue<int > q;
memset(vis,0,sizeof(vis));
q.push(1);vis[1] = 1;minn[1] = a[1];
while(!q.empty())
{
u = q.front();
q.pop();
vis[u] = 0;
for(int i = head[u];i;i = e[i].nxt)
if(e[i].tag == 2 || e[i].tag == 0)
{
v = e[i].to;
if(minn[v] > minn[u] || minn[v] > a[v])
{
minn[v] = min(minn[u],a[v]);
if(!vis[v])
{
q.push(v);
vis[v] = 1;
}
}
}
}
}
int main()
{
// freopen("P1073_1.in","r",stdin);
scanf("%d%d",&n,&m);
for(int i = 1;i <= n;i ++)
{
scanf("%d",&a[i]);
minn[i] = inf;maxn[i] = -1;
}
for(int i = 1;i <= m;i ++)
{
scanf("%d%d%d",&x,&y,&z);
if(z == 2)
{
addedge(x,y,2);
addedge(y,x,2);
}
else
{
addedge(x,y,0);
addedge(y,x,1);
}
}
SPFA();
SPFA_neg();
for(int i = 2;i < n;i ++)
if(minn[i] != inf && maxn[i] != -1)
ans = max(ans,maxn[i]-minn[i]);
cout << ans;
return 0;
}
树形DP
P1273 有线电视网
分组背包:P1273 有线电视网
简单来说就是把每个节点看成一个背包啦,它的容量就是以这个节点为根的子树大小,组数就是连接的儿子个数。
每组都有很多选择,选一个,两个,三个用户,把这些选择当做组中的元素就好了,容易看出每组中只能选一个元素,比如你选择了选一个用户,就不可能同时选择选两个用户。
#include<iostream>
#include<cstdio>
using namespace std;
const int inf = 0xfffffff;
int n,m,x,y,z;
int head[3005],cnt,tmp,f[3005][3005],a[3005];
int ans,maxn[3005];
struct edge{
int nxt;
int to;
int fee;
}e[80005];
void addedge(int from,int to,int cost)
{
cnt ++;
e[cnt].nxt = head[from];
e[cnt].to = to;
e[cnt].fee = cost;
head[from] = cnt;
return ;
}
void dfs_dp(int now)
{
if(head[now] == 0)
{
f[now][1] = a[now];
maxn[now] = 1;
return ;
}
int son = 0;
for(int i = head[now];i;i = e[i].nxt)
{
son = e[i].to;
dfs_dp(son);
maxn[now] += maxn[son];
for(int j = maxn[now];j >= 0;j --)
for(int k = 1;k <= maxn[son];k ++)
if(j-k >= 0) f[now][j] = max(f[now][j],f[now][j-k]+f[son][k]-e[i].fee);
}
return ;
}
int main()
{
scanf("%d%d",&n,&m);
for(int i = 1;i <= n-m;i ++)
{
scanf("%d",&x);
for(int j = 1;j <= x;j ++)
{
scanf("%d%d",&y,&z);
addedge(i,y,z);
}
}
for(int i = n-m+1;i <= n;i ++) scanf("%d",&a[i]);
for(int i = 1;i <= n;i ++)
for(int j = 1;j <= m;j ++)
f[i][j] = -inf;
dfs_dp(1);
for(int i = m;i >= 0;i --)
if(f[1][i] >= 0)
{
ans = i;
break;
}
printf("%d",ans);
return 0;
}
P2585 [ZJOI2006]三色二叉树
染色问题?:P2585 [ZJOI2006]三色二叉树
设
f
(
u
,
c
)
f(u,c)
f(u,c) 表示以
u
u
u 为根结点且
u
u
u 的颜色为
c
c
c 的子树下绿色结点的个数
显然易得:
f
max
(
u
,
c
)
=
max
{
f
m
a
x
(
l
s
o
n
,
(
c
+
1
)
m
o
d
3
)
+
f
m
a
x
(
r
s
o
n
,
(
c
+
2
)
m
o
d
3
)
+
[
c
=
g
r
e
e
n
]
f
m
a
x
(
l
s
o
n
,
(
c
+
2
)
m
o
d
3
)
+
f
m
a
x
(
r
s
o
n
,
(
c
+
1
)
m
o
d
3
)
+
[
c
=
g
r
e
e
n
]
f
m
i
n
(
u
,
c
)
=
min
{
f
m
i
n
(
l
s
o
n
,
(
c
+
1
)
m
o
d
3
)
+
f
m
i
n
(
r
s
o
n
,
(
c
+
2
)
m
o
d
3
)
f
m
i
n
(
l
s
o
n
,
(
c
+
2
)
m
o
d
3
)
+
f
m
i
n
(
r
s
o
n
,
(
c
+
1
)
m
o
d
3
)
+
[
c
=
g
r
e
e
n
]
f_{\max}(u,c) = \max \begin{cases} f_{max}(lson,(c+1)\mod3) + f_{max}(rson,(c+2)\mod3)+[c=green] \\ f_{max}(lson,(c+2)\mod3) + f_{max}(rson,(c+1)\mod3)+[c=green] \\ \end{cases} \\ \\ f_{min}(u,c) = \min \begin{cases} f_{min}(lson,(c+1)\mod 3) + f_{min}(rson,(c+2)\mod3) \\ f_{min}(lson,(c+2)\mod 3) + f_{min}(rson,(c+1)\mod 3) \\ \end{cases} +[c=green]
fmax(u,c)=max{fmax(lson,(c+1)mod3)+fmax(rson,(c+2)mod3)+[c=green]fmax(lson,(c+2)mod3)+fmax(rson,(c+1)mod3)+[c=green]fmin(u,c)=min{fmin(lson,(c+1)mod3)+fmin(rson,(c+2)mod3)fmin(lson,(c+2)mod3)+fmin(rson,(c+1)mod3)+[c=green]
#include<iostream>
#include<cstdio>
#include<string>
#include<cstring>
using namespace std;
const int inf = 0xfffffff;
string s;
int n,cnt,fa[500005],lson[500005],rson[500005];
int fmax[500005][3],fmin[500005][3];//0 red,1 blue,2 green
bool vis[500005];
void dfs_dp(int now)
{
if(s[now-1] == '0')
{
fmax[now][0] = fmin[now][0] = 0;
fmax[now][1] = fmin[now][1] = 0;
fmax[now][2] = fmin[now][2] = 1;
return ;
}
if(s[now-1] == '1')
{
dfs_dp(lson[now]);
fmax[now][0] = max(fmax[now][0],max(fmax[lson[now]][1],fmax[lson[now]][2]));
fmax[now][1] = max(fmax[now][1],max(fmax[lson[now]][0],fmax[lson[now]][2]));
fmax[now][2] = max(fmax[now][2],max(fmax[lson[now]][0],fmax[lson[now]][1])+1);
fmin[now][0] = min(fmin[now][0],min(fmin[lson[now]][1],fmin[lson[now]][2]));
fmin[now][1] = min(fmin[now][1],min(fmin[lson[now]][0],fmin[lson[now]][2]));
fmin[now][2] = min(fmin[now][2],min(fmin[lson[now]][0],fmin[lson[now]][1]))+1;
return ;
}
if(s[now-1] == '2')
{
dfs_dp(lson[now]);dfs_dp(rson[now]);
fmax[now][0] = max(fmax[now][0],max(fmax[lson[now]][1] + fmax[rson[now]][2],fmax[lson[now]][2] + fmax[rson[now]][1]));
fmax[now][1] = max(fmax[now][1],max(fmax[lson[now]][0] + fmax[rson[now]][2],fmax[lson[now]][2] + fmax[rson[now]][0]));
fmax[now][2] = max(fmax[now][2],max(fmax[lson[now]][1] + fmax[rson[now]][0],fmax[lson[now]][0] + fmax[rson[now]][1])+1);
fmin[now][0] = min(fmin[now][0],min(fmin[lson[now]][1] + fmin[rson[now]][2],fmin[lson[now]][2] + fmin[rson[now]][1]));
fmin[now][1] = min(fmin[now][1],min(fmin[lson[now]][0] + fmin[rson[now]][2],fmin[lson[now]][2] + fmin[rson[now]][0]));
fmin[now][2] = min(fmin[now][2],min(fmin[lson[now]][1] + fmin[rson[now]][0],fmin[lson[now]][0] + fmin[rson[now]][1]))+1;
return ;
}
}
void dfs_build(int now)
{
vis[now] = 1;
if(s[now-1] == '0') return ;
if(s[now-1] == '2')
{
for(int i = now+1;i <= n;i ++)
if(!vis[i])
{
lson[now] = i;fa[i] = now;
dfs_build(i);
break;
}
for(int i = now+1;i <= n;i ++)
if(!vis[i])
{
rson[now] = i;fa[i] = now;
dfs_build(i);
break;
}
return ;
}
if(s[now-1] == '1')
{
for(int i = now + 1;i <= n;i ++)
if(!vis[i])
{
lson[now] = i;fa[i] = now;
dfs_build(i);
break;
}
return ;
}
}
int main()
{
cin >> s;n = s.size();
dfs_build(1);
for(int i = 1;i <= n;i ++)
{
fmin[i][0] = fmin[i][1] = fmin[i][2] = inf;
}
dfs_dp(1);
cout << max(fmax[1][0],max(fmax[1][1],fmax[1][2])) << ' ' << min(fmin[1][0],min(fmin[1][1],fmin[1][2])) << endl;
return 0;
}
ZJOI2007时态同步
P1131 [ZJOI2007]时态同步
只能算半个DP,因为在想到DP之前就已经想到dfs暴力做法了
发现的重要性质是:对于一些叶子结点,在它们的公共祖先与其父亲的一条边上加操作显然比其它方案更优。
于是瞎搞一个有点像差分的东西,算出答案
#include<iostream>
#include<cstdio>
using namespace std;
typedef long long ll;
int n,s,x,y;
int head[500005],cnt,fa[500005];
ll d[500005],fmax[500005],add[500005],maxn,ans,z;
struct edge{
int nxt;
int to;
ll dis;
}e[1000100];
void addedge(int from,int to,ll val)
{
cnt ++;
e[cnt].nxt = head[from];
e[cnt].to = to;
e[cnt].dis = val;
head[from] = cnt;
return ;
}
void dfs1(int u,int father)
{
int v = 0;bool leaf = 1;
for(int i = head[u];i;i = e[i].nxt)
{
v = e[i].to;
if(v != father)
{
fa[v] = u;leaf = 0;
d[v] = d[u] + e[i].dis;
dfs1(v,u);
fmax[u] = max(fmax[u],fmax[v]);
}
}
if(leaf) fmax[u] = d[u];
maxn = max(maxn,fmax[u]);
return ;
}
void dfs2(int u)
{
int v = 0;
for(int i = head[u];i;i = e[i].nxt)
{
v = e[i].to;
if(v != fa[u])
dfs2(v);
}
add[u] -= add[fa[u]];
ans += add[u];
return ;
}
int main()
{
scanf("%d",&n);
scanf("%d",&s);
for(int i = 1;i < n;i ++)
{
scanf("%d%d%lld",&x,&y,&z);
addedge(x,y,z);
addedge(y,x,z);
}
d[s] = 0;
dfs1(s,0);
for(int i = 1;i <= n;i ++) add[i] = maxn - fmax[i];
dfs2(s);
printf("%lld",ans);
return 0;
}