8 : 00 ∼ 10 : 00 \qquad 8:00\sim 10:00 8:00∼10:00 开 T 1 T1 T1。第一眼看上去并没有明确的思路,简单想了想发现可以直接贪心走,不过好像有点小细节。推了推细节发现还是用线段树维护写起来顺一点。 9 : 40 9:40 9:40 写完并调完第一发。不过他题目给的 n = 5 × 1 0 5 n=5\times 10^5 n=5×105,而我用线段树的频率异常之高,害怕被卡常就测了一发极限数据。一测发现线段树有一点写挂了!给的大样例压根就没测出来……简单一改加个快读就跳了。
10 : 00 ∼ 10 : 30 \qquad 10:00\sim 10:30 10:00∼10:30 开 T 2 T2 T2。一眼看上去感觉 T 2 T2 T2 并不难,很快想到了一个莫队计算区间出现数字种类数的做法,但是 n = 5 × 1 0 5 n=5\times 10^5 n=5×105。总感觉这个问题很典,但是并没想出来怎么做。中间想过主席树,不过也没想出来咋用。于是乎就先把莫队写了,拿个 60 p t s 60pts 60pts。
10 : 30 ∼ 10 : 50 \qquad 10:30\sim 10:50 10:30∼10:50 开 T 3 T3 T3。看到极差就觉得肯定是个套路题,但是不知道极差有啥套路……开始想二分极差,但是发现搞不了。接着就想二分最大值,然后 d p dp dp 出来最小值最大能是多少。不过细想了一下发现二分好像不是很对,好像得三分。凭着感觉写完了三分套 d p dp dp,调了一小会过了样例就没再管。
10 : 50 ∼ 11 : 50 \qquad 10:50\sim 11:50 10:50∼11:50 开 T 4 T4 T4。 T 4 T4 T4 也能看出来是套路题(指统计贡献),但是感觉太麻烦了,而且无思路,就开始想暴力。 O ( n 3 ) O(n^3) O(n3) 很好搞,不过并没有这一档部分分!开始想怎么搞 O ( n 2 ) O(n^2) O(n2)。口胡了一个枚举根,在遍历树的时候顺带计算一下答案。感觉很对就写了,写完就发现寄了。很多情况没考虑到。缝缝补补调了一大晌,手玩样例发现这个做法好像就不对……然后就罚坐到了比赛结束。
95 + 60 + 100 + 0 = 255 p t s \qquad 95 + 60 + 100 + 0 = 255pts 95+60+100+0=255pts, R a n k : 1 Rank:1 Rank:1(虽然只有两个人)。 T 1 T1 T1 线段树终究被卡常了……
\qquad 意识到 T 1 T1 T1 可能被卡常后就应该再深入剖析一下线段树做的操作是什么。维护 S u m Sum Sum 是单点修改,区间查询,维护 D a y Day Day 是区间修改单点查询,这两者都可以用常数更小的树状数组来维护。而对于 D a t Dat Dat 完全可以用 S T ST ST 表代替。
\qquad T 2 T2 T2 确实很典,树状数组就能搞。以前的一些知识、套路还是掌握的不牢固。
\qquad 一个显而易见的性质:如果遇到了一个会让小 P P P 破产的点 i i i,那么我们一定会在 1 ∼ i 1\sim i 1∼i 中收益最大的点积攒经济直到可以通过点 i i i。模拟上述过程即可。可以用树状数组搞到 O ( n log n ) O(n\log n) O(nlogn),也可以直接用一个变量来储存偏移量,因为我们只会用到上一个位置和前缀最值的信息。前缀最值预处理一下即可,整体复杂度就可以降至 O ( n ) O(n) O(n)。
\qquad 我们显然不能直接枚举 k k k,考虑我们可以怎样给 k k k 贡献答案。对于一个位置 i i i,如果 a i a_i ai 之前没有出现过,那么我们要给所有的 k k k 贡献 1 1 1 的插入次数。如果 a i a_i ai 之前出现过,记上一次出现过的位置为 l s t i lst_i lsti。假设 ( l s t i , i ) (lst_i,i) (lsti,i) 之间的数字种类数为 n u m num num,那么我们会对所有 k ≤ n u m k\leq num k≤num 的 k k k 贡献 1 1 1 的插入次数。证明显然。这样我们就把问题转化为:一共有 m m m 个形如 [ l s t i + 1 , i − 1 ] [lst_i+1,i-1] [lsti+1,i−1] 的区间,对于每一个区间,求出区间内出现过的数字的种类数。假设种类数为 n u m num num,那么我们让 s n u m s_{num} snum 加一,最后求一个 s s s 的后缀和就是答案。现在,我们考虑如何快速求一段区间内出现过的数字的种类数。
\qquad 这个问题是不是很熟悉?我们将每个区间挂在区间右端点处,然后从左到右扫区间右端点。对于点 i i i,我们在树状数组上让 l s t i lst_i lsti 减一,让 i i i 加一,对于每一个区间我们求区间和即可。时间复杂度 O ( n log n ) O(n\log n) O(nlogn)。
\qquad 核心 C o d e : Code: Code:
for(int i = 1; i <= m; i ++) {
if(lst[i]) T.add(lst[i], -1);
T.add(i, 1);
int Now = T.ask(i);
for(auto x : pos[i]) {
int ans = T.ask(x - 1);
s[Now - ans] ++;
}
}
\qquad 直接维护最小极差是不好维护的。二分极差后 c h e c k check check 也是非常难搞的。但是我们注意到,如果知道当前集合中的最大值,那么我们只要尽可能地让最小值最大,就可以最小化极差。所以我们可以考虑二分最大值……吗?
\qquad 直接二分是不行的,因为如果最大值过大,大到序列中任何两个数都拼不出这个最大值,那这个最大值也一定是不优的。所以我们考虑三分最大值。(不过在这想道题的过程中,我突然发现三分也可以求出二分的最优值!好愚蠢地发现啊)最大值固定后,让最小值最大就好搞了,简单 d p dp dp 即可。设 d p i , 0 / 1 / 2 dp_{i,0/1/2} dpi,0/1/2 表示第 i i i 个数 \, 未与前面结合,不新开一段/未与前面结合,新开一段/与前面结合,新开一段 \, 能得到的最小值。转移很好转。
\qquad 核心 C o d e : Code: Code:
int check(int x) {
memset(dp, 0x7f, sizeof dp);
for(int i = 1; i <= n; i ++) {
if(a[i] + a[i - 1] <= x) dp[i][2] = min(dp[i - 1][0], a[i] + a[i - 1]);//能与前面结合
if(dp[i - 1][2] != 0x7f7f7f7f) dp[i][1] = min(max(dp[i - 1][1], dp[i - 1][2]), a[i]);//新开一段
else dp[i][1] = min(dp[i - 1][1], a[i]);
if(dp[i - 1][2] != 0x7f7f7f7f) dp[i][0] = max(dp[i - 1][1], dp[i - 1][2]);//不新开一段
else dp[i][0] = dp[i - 1][1];
}
//由于dp[n][0]并没有将n归到某一段内,所以是一个不合法的状态
if(dp[n][2] == 0x7f7f7f7f) return x - dp[n][1];
return x - max(dp[n][1], dp[n][2]);
}
\qquad 看到这类题,统计贡献这个套路应该立马浮现在脑中。在这一题中,我们便考虑统计一个点对 ( x , y ) (x,y) (x,y) 的贡献。对于点对 ( x , y ) (x,y) (x,y),我们不妨假设 w x < w y w_x
\qquad 情况 1 1 1:五号节点与图中 A A A 部分节点组成的点对。经过这些点对的路径条数就是( B , C B,C B,C 部分点数加 1 1 1(五号点本身))乘 A A A 部分点子树大小之和。情况 2 2 2:五号节点与图中 B B B 部分节点组成的点对。经过这些点对的路径条数为五号点子树大小乘( C C C 部分点数加 B B B 部分节点到根这条链上的点数)。情况 3 3 3:五号节点与图中 C C C 部分节点组成的点对。这部分的贡献我们无法直接计算,只能用容斥原理,路径条数为五号节点子树大小乘(所有小于五号节点的点的子树大小之和减五号节点子树内的符合要求的点的子树和减五号节点到根这条链上的符合要求的点的子树和)。实现上细节很多。由于根节点比较特殊,所以根节点最好单独计算。
\qquad C o d e Code Code:
#include
using namespace std;
#define int long long
typedef long long LL;
const int maxn = 5e5 + 10, mod = 1e9 + 7;
inline void myplus(int &x, int y) {x = (x + y >= mod ? x + y - mod : x + y);}
int n;
int a[maxn];
vector < int > to[maxn];
vector < int > xs;
int ans, sze[maxn], s[maxn];
vector < int > sz[maxn];
int two;
struct Segment {
int ls, rs, sum;
#define ls(x) tree[x].ls
#define rs(x) tree[x].rs
#define sum(x) tree[x].sum
}tree[maxn * 60];
int pnt, root[maxn];
struct Fenwick {
int c[maxn];
inline int lowbit(int x) {return x & (-x);}
void add(int x, int y) {while(x <= n) myplus(c[x], y), x += lowbit(x);}
int ask(int x) {int res = 0; while(x) myplus(res, c[x]), x -= lowbit(x); return res;}
}T, TS;
inline int build() {pnt ++, ls(pnt) = rs(pnt) = sum(pnt) = 0; return pnt;}
inline void update(int p) {
sum(p) = 0;
myplus(sum(p), sum(ls(p)) + sum(rs(p)));
}
void Insert(int p, int l, int r, int x, int val) {
if(l == r) return myplus(sum(p), val), void();
int mid = l + r >> 1;
if(x <= mid) ls(p) = build(), Insert(ls(p), l, mid, x, val);
else rs(p) = build(), Insert(rs(p), mid + 1, r, x, val);
update(p);
}
int merge(int p, int q, int l, int r) {
if(!p || !q) return (p ^ q);
if(l == r) return myplus(sum(p), sum(q)), p;
int mid = l + r >> 1;
ls(p) = merge(ls(p), ls(q), l, mid), rs(p) = merge(rs(p), rs(q), mid + 1, r);
update(p);
return p;
}
int query(int p, int l, int r, int ql, int qr) {
if(ql > qr) return 0;
if(ql <= l && r <= qr) return sum(p);
int mid = l + r >> 1, sum = 0;
if(ql <= mid && ls(p)) myplus(sum, query(ls(p), l, mid, ql, qr));
if(qr > mid && rs(p)) myplus(sum, query(rs(p), mid + 1, r, ql, qr));
return sum;
}
int qpow(int a, int b) {
int res = 1;
while(b) {
if(b & 1) res = (1LL * res * a) % mod;
a = (1LL * a * a) % mod, b >>= 1;
}
return res % mod;
}
void dfs_pre(int x, int fa) {
sze[x] = 1;
for(int v : to[x]) {
if(v == fa) continue;
dfs_pre(v, x);
sze[x] += sze[v];
}
}
void dfs(int x, int fa) {
if(x != 1) myplus(ans, (1LL * (1LL * xs[a[x] - 1] * T.ask(a[x] - 1)) % mod * sze[x]) % mod);
Insert(root[x], 1, n, a[x], sze[x]);//子树内点对的计算采取线段树合并的实现方式
TS.add(a[x], sze[x]);
for(int v : to[x]) {
if(v == fa) continue;
T.add(a[x], n - sze[v]);
dfs(v, x);
T.add(a[x], -n + sze[v] + mod);
root[x] = merge(root[x], root[v], 1, n);
if(x != 1) myplus(ans, (1LL * (1LL * xs[a[x] - 1] * (n - sze[v])) % mod * query(root[v], 1, n, 1, a[x] - 1)) % mod);
}
//这里计算过程中都只统计了a[x]-1的点的贡献,自己的贡献留到后面单独计算
int son = query(root[x], 1, n, 1, a[x] - 1);
if(x != 1) myplus(ans, (1LL * (1LL * xs[a[x] - 1] * (s[a[x] - 1] - TS.ask(a[x] - 1) - son) % mod) % mod * sze[x]) % mod);
TS.add(a[x], -sze[x] + mod);
}
void d_r(int x, int fa, int Sze) {
if(x == 1) {
for(int v : to[x]) {
d_r(v, x, sze[v]);
}
}
else {
if(a[x] < a[1]) myplus(ans, (1LL * (1LL * xs[a[1] - 1] * sze[x]) % mod * (n - Sze)) % mod);
for(int v : to[x]) {
if(v == fa) continue;
d_r(v, x, Sze);
}
}
}
void DP_pre(int x, int fa) {
for(int v : to[x]) {
if(v == fa) continue;
DP_pre(v, x);
sz[x].push_back(sze[v]);
}
}
void DP(int x, int fa) {//简单换根dp
int all = 0, sum = 0;
for(int i : sz[x]) myplus(all, i), myplus(sum, (1LL * xs[a[x] - 1] * i) % mod);
int res = 0;
for(int i : sz[x]) myplus(res, (1LL * (1LL * xs[a[x] - 1] * (all - i)) % mod * i) % mod);
res = (1LL * res * two) % mod;
myplus(ans, res), myplus(ans, sum);
for(int v : to[x]) {
if(v == fa) continue;
sz[v].push_back(n - sze[v]);
DP(v, x);
}
}
signed main() {
scanf("%lld", &n);
for(int i = 1; i <= n; i ++) scanf("%lld", &a[i]), xs.emplace_back(a[i]), root[i] = build();
sort(xs.begin(), xs.end()), xs.erase(unique(xs.begin(), xs.end()), xs.end());
for(int i = 1; i <= n; i ++) a[i] = lower_bound(xs.begin(), xs.end(), a[i]) - xs.begin() + 1;
for(int i = 1, x, y; i < n; i ++) scanf("%lld%lld", &x, &y), to[x].emplace_back(y), to[y].emplace_back(x);
dfs_pre(1, 0);
//子树内 + 链
for(int i = 1; i <= n; i ++) s[a[i]] = sze[i];//求一下sze前缀和,容斥部分用
for(int i = 1; i <= n; i ++) myplus(s[i], s[i - 1]);
dfs(1, 0);
//单独处理根的情况
d_r(1, 0, 0);
//单独处理每个点被包含在多少条路径中,即自己与自己构成的点对
two = qpow(2, mod - 2);
DP_pre(1, 0);
DP(1, 0);
for(int i = 1; i <= n; i ++) myplus(ans, xs[a[i] - 1]);
printf("%lld\n", ans);
return 0;
}