杭电多校第五场8月3日补题记录

D Another String

题意:给定一个长度为 n n n 的字符串,考虑从每一个位置将字符串切开。记从第 i i i 位切开得到的两个子串 S [ 1 ⋯ i ] = A S[1 \cdots i]=A S[1i]=A S [ i + 1 ⋯ n ] = B S[i+1 \cdots n]=B S[i+1n]=B,求对于每一个位置下 A , B A,B A,B 两个字符串的全部等长连续子串错配次数不超过 k k k 次的方案总数。 n ≤ 2 × 1 0 3 n \leq 2\times 10^3 n2×103

解法:首先考虑一个子问题,从第 i i i 位开始的一个连续子串和后面相距为 d d d 的等长连续子串,其错配次数不超过 k k k 的最长连续合法子串长度。记从 i i i 开始匹配与从 j j j 开始匹配的错配次数不超过 k k k 的长度为 f i , j f_{i,j} fi,j

这个子问题可以考虑使用尺取法(双指针)解决。考虑使用指针 l , r l,r l,r 维护当前的最长错配次数不超过 k k k 次的区间。当左指针 l l l 右移一位时,右指针一定不会向左移动。因而可以在 d d d 固定的情况下 O ( n ) O(n) O(n) 的解决 ∀ i ∈ [ 1 , n − d ] \forall i \in [1,n-d] i[1,nd] 下的问题。记这个问题答案为 f i , i + d f_{i,i+d} fi,i+d,只需要 O ( n ) O(n) O(n) 的枚举 d d d 即可。这个子问题处理复杂度 O ( n 2 ) O(n^2) O(n2)

接下来考虑进行划分,计算每一个 f i , j f_{i,j} fi,j 对在 i i i 处划分的贡献,下面假如划分点在 t t t 处。可以知道,在此处的答案为 a n s t = ∑ i = 1 t − 1 ∑ j = t n min ⁡ ( f i , j , t − i ) ans_t=\displaystyle \sum_{i=1}^{t-1} \sum_{j=t}^{n} \min(f_{i,j},t-i) anst=i=1t1j=tnmin(fi,j,ti)。其含义为,考虑固定左侧开始匹配点 i i i,对于给定的 i i i 其方案总数为 ∑ j = t n min ⁡ ( f i , j , t − i ) \displaystyle \sum_{j=t}^{n} \min(f_{i,j},t-i) j=tnmin(fi,j,ti),因为需要枚举右侧开始匹配的点,同时它们最长匹配长度不能超过 t − i t-i ti,否则左侧就超过了分界线。这样长度在 min ⁡ ( f i , j , t − i ) \min(f_{i,j},t-i) min(fi,j,ti) 以下的每一个子串都可以成为贡献。

显然无法暴力直接计算这个东西,我们考虑使用局部调整的策略。假如我们知道了 a n s t ans_t anst,我们可否推出 a n s t − 1 ans_{t-1} anst1 或者 a n s t + 1 ans_{t+1} anst+1 呢?容易发现,每一轮计算中求和部分的最大值不超过 t − i t-i ti,因而可以考虑倒序枚举 t t t,这样最大值就会单调递减。同时这些值也不算多(小于 t − i t-i ti,对于全局就是小于 n n n),因而可以使用一个桶来维护这些可行的值出现了多少次。

对于每一个给定的 i i i,我们都使用一个桶。记二维数组 c n t i , x cnt_{i,x} cnti,x 表示 min ⁡ ( f i , j , t − i ) \min(f_{i,j},t-i) min(fi,j,ti) 出现的次数,记上一轮答案为 a n s t ans_t anst。那么容易发现, a n s t ans_t anst 里包括了 t − 1 t-1 t1 的情况即 ∑ j = t n min ⁡ ( f t − 1 , j , 1 ) \displaystyle \sum_{j=t}^{n} \min(f_{t-1,j},1) j=tnmin(ft1,j,1),我们先把这一部分减掉。然后,需要对于 ∀ i ∈ [ 1 , t − 2 ] \forall i \in [1,t-2] i[1,t2] 上增补一个 min ⁡ ( f i , t − 1 , t − 1 − i ) \min(f_{i,t-1},t-1-i) min(fi,t1,t1i)。此外,对于剩下全部的,如果当前 f i , j > t − i − 1 f_{i,j}>t-i-1 fi,j>ti1 的,全部都要减一。处理完这三个部分,我们就由 a n s t ans_t anst 转移到了 a n s t − 1 ans_{t-1} anst1 了。注意到每次 t t t 的移动,至多只会增加减少 O ( n ) O(n) O(n) 个元素,因而整体复杂度 O ( n 2 ) O(n^2) O(n2)。详细细节可以见代码。

#include <cstdio>
#include <algorithm>
using namespace std;
int f[3005][3005], maximum[3005], cnt[3005][3005], ans[3005];
char a[3005];
int main()
{
    int T, n, k;
    scanf("%d", &T);
    while(T--)
    {
        scanf("%d%d", &n, &k);
        for (int i = 1; i <= n;i++)
            for (int j = 1; j <= n;j++)
                f[i][j] = cnt[i][j] = 0;
        for (int i = 1; i <= n;i++)
            maximum[i]=n;
        scanf("%s", a + 1);
        //双指针法
        for (int d = 2; d <= n;d++)
        //枚举差值
        {
            int dif = 0, left = 1, right = 0;
            //对于前面的串[left,right] 与 后面的串[left+d-1,right+d-1] 的错配情况为 dif。
            for (; left + d - 1 <= n; left++)
            {
                while (right + d <= n && dif <= k)
                //如果预先判断再处理,就可以规避移动后发现条件不满足又要反向移动的问题。
                {
                    right++;
                    if (a[right] != a[right + d - 1])
                        dif++;
                }
                f[left][left + d - 1] = right - left + (dif <= k);
                if (a[left] != a[left + d - 1])
                    dif--;
            }
        }
        long long now = 0;//现有的答案
        for (int t = n; t >= 2; t--)
        {
            for (int i = 1; i < t;i++)
            //只需要处理 [1,t-1] 的情况,其余的都已经不在统计口径内了
                while(maximum[i]>t-i)//maximum[i] 表示了第i组桶内的最大值。此处即是卡t-i的。
                {
                    now -= cnt[i][maximum[i]];
                    //这里直接不是减次数,而是先减了一个 cnt[i][maximum[i]]*maximum[i] 后下面又补了一个 cnt[i][maximum[i]]*(maximum[i]-1)
                    cnt[i][maximum[i] - 1] += cnt[i][maximum[i]];
                    //承接
                    maximum[i]--;
                }
            for (int i = 1; i < t;i++)
            {
                int temp = min(f[i][t], t - i);
                //对于每一个i下都需要新增j=t的情况,即 Min(f[i][t],t-i)
                now += temp;
                cnt[i][temp]++;
            }
            ans[t] = now;
            while(maximum[t-1]>0)
            {
                now -= cnt[t - 1][maximum[t - 1]] * maximum[t - 1];
                //抹去i=t-1的情况。
                maximum[t - 1]--;
            }
        }
        for (int i = 2; i <= n;i++)
            printf("%d\n", ans[i]);
    }
    return 0;
}

E Random Walk 2

题意:在一张 n n n 个点的无向完全图上随机游走,从 i i i 出发到 j j j 的概率为 p i , j p_{i,j} pi,j,如果从任意的 i i i 又走到了 i i i 则永远停下,问从一个点出发到每一个点结束的概率。需要求出每一个点出发的情况。 n ≤ 300 n \leq 300 n300

解法:题目所求的 A i , j A_{i,j} Ai,j 为从 i i i 点出发,最后停在 j j j 点的概率。记 p i , j p_{i,j} pi,j 为从 i i i 点转移到 j j j 点的概率。那么可以写出

  1. A i , i = p i , i + ∑ k = 1 , k ≠ i n p i , j A j , i \displaystyle A_{i,i}=p_{i,i} +\sum_{k=1,k \neq i}^{n} p_{i,j} A_{j,i} Ai,i=pi,i+k=1,k=inpi,jAj,i,即当前可以以 p i , j p_{i,j} pi,j 的概率走出去到 j j j,最后又从 j j j 这里走回来游戏结束;也可以是直接当前走到自己然后结束。由于游戏局数没有限制,因而第一步的归属问题不用在意。’
  2. A i , j = ∑ k = 1 , k ≠ i n p k , j A k , i \displaystyle A_{i,j}=\sum_{k=1,k \neq i}^{n} p_{k,j} A_{k,i} Ai,j=k=1,k=inpk,jAk,i,即当前可以走出去然后在别的点出发结束游戏。

对于整个矩阵 A A A,其递推关系是由两部分构成的——一个是对角线元素,一个是其他元素。因而将原式写成矩阵形式的时候也应该分开考虑这两部分的影响。如果我们记 Λ = [ p 1 , 1 p 2 , 2 ⋱ p n , n ] \Lambda=\begin{bmatrix} p_{1,1} & & & \\ &p_{2,2} & & \\&&\ddots &\\&&&p_{n,n} \end{bmatrix} Λ=p1,1p2,2pn,n P P P 为全部的概率矩阵即 P = [ p 1 , 1 p 1 , 2 ⋯ p 1 , n p 2 , 1 p 2 , 2 ⋯ p 2 , n ⋮ ⋮ ⋱ ⋮ p n , 1 p n , 2 ⋯ p n , n ] P=\begin{bmatrix} p_{1,1} & p_{1,2}& \cdots& p_{1,n}\\ p_{2,1}&p_{2,2} & \cdots & p_{2,n}\\\vdots &\vdots &\ddots &\vdots\\p_{n,1}&p_{n,2}&\cdots&p_{n,n} \end{bmatrix} P=p1,1p2,1pn,1p1,2p2,2pn,2p1,np2,npn,n B = P − Λ = [ 0 p 1 , 2 ⋯ p 1 , n p 2 , 1 0 ⋯ p 2 , n ⋮ ⋮ ⋱ ⋮ p n , 1 p n , 2 ⋯ 0 ] B=P-\Lambda=\begin{bmatrix} 0& p_{1,2}& \cdots& p_{1,n}\\ p_{2,1}&0& \cdots & p_{2,n}\\\vdots &\vdots &\ddots &\vdots\\p_{n,1}&p_{n,2}&\cdots&0 \end{bmatrix} B=PΛ=0p2,1pn,1p1,20pn,2p1,np2,n0,则可以写出:

B A + Λ = A BA+\Lambda=A BA+Λ=A

化简一下就有 A = ( I − B ) − 1 Λ A=(I-B)^{-1}\Lambda A=(IB)1Λ,即 A = ( I − P + Λ ) − 1 Λ A=(I-P+\Lambda)^{-1}\Lambda A=(IP+Λ)1Λ

#include <cstdio>
#include <algorithm>
#include <vector>
using namespace std;
const long long mod = 998244353ll;
long long power(long long a,long long x)
{
    long long ans = 1;
    while(x)
    {
        if(x&1)
            ans = ans * a % mod;
        a = a * a % mod;
        x >>= 1;
    }
    return ans;
}
vector<vector<long long>> get_inv(int n,vector<vector<long long>> &a)
{
    vector<vector<long long>> ans(n + 1, vector<long long>(n + 1, 0));
    int m = 2 * n;
    for (int i = 1; i <= n; i++)
    {
        int place = i;
        for (int j = i + 1; j <= n; j++)
            if (abs(a[j][i]) > abs(a[place][i]))
                place = j;
        if (i != place)
            swap(a[i], a[place]);
        long long inv = power(a[i][i], mod - 2);
        for (int j = 1; j <= n; j++)
            if (j != i)
            {
                long long multiple = a[j][i] * inv % mod;
                for (int k = i; k <= m; k++)
                    a[j][k] = ((a[j][k] - a[i][k] * multiple) % mod + mod) % mod;
            }
        for (int j = 1; j <= m; j++)
            a[i][j] = (a[i][j] * inv % mod);
    }
    for (int i = 1; i <= n; i++)
        for (int j = n + 1; j <= m; j++)
            ans[i][j - n] = a[i][j];
    return ans;
}
long long p[305][305], w[305][305];
int main()
{
    int n, t;
    scanf("%d", &t);
    while(t--)
    {
        scanf("%d", &n);
        for (int i = 1; i <= n;i++)
        {
            long long sum = 0;
            for (int j = 1; j <= n;j++)
            {
                scanf("%lld", &w[i][j]);
                sum += w[i][j];
            }
            long long invsum = power(sum, mod - 2);
            for (int j = 1; j <= n;j++)
                p[i][j] = w[i][j] * invsum % mod;
        }
        vector<vector<long long>> num(n + 1, vector<long long>(2 * n + 1, 0));
        for (int i = 1; i <= n;i++)
        {
            for (int j = 1; j <= n;j++)
            {
                if(i==j)
                    num[i][j] = 1;
                else
                    num[i][j] = (-p[i][j] % mod + mod) % mod;
            }
            num[i][i + n] = p[i][i];
        }
        vector<vector<long long>> ans = get_inv(n, num);
        for (int i = 1; i <= n;i++)
        {
            for (int j = 1; j <= n;j++)
            {
                printf("%lld", ans[i][j]);
                if(j!=n)
                    printf(" ");
            }
            printf("\n");
        }
    }
    return 0;
}

H Supermarket

题意:给定 n n n 个数 a i a_i ai m m m,有 a i ∈ [ 0 , 2 m ) a_i \in [0,2^m) ai[0,2m),每一个 a i a_i ai 都表示了 m m m 件物品中购买了哪一些。定义条件概率 P ( T ∣ S ) P(T|S) P(TS) 为当购买集合为 S S S 的时候,有多少的概率能从完全包含 S S S 集合的集合中取到 T T T。求 ∑ T ∈ n ∑ S ∈ n P ( T ∣ S ) \displaystyle \sum_{T \in n} \sum_{S \in n} P(T|S) TnSnP(TS) n ≤ 1 × 1 0 5 n \leq 1\times 10^5 n1×105

解法:将概率转化为个数比。记 c n t ( S ) cnt(S) cnt(S) S S S 超集个数,显然原式可以转化 ∑ T ∈ n ∑ S ∈ n c n t ( T ∪ S ) c n t ( S ) \displaystyle \sum_{T \in n} \sum_{S \in n} \frac{cnt(T \cup S)}{cnt(S)} TnSncnt(S)cnt(TS),即包含 S S S 集合的全部集合作为全集,又满足 T T T 又满足 S S S 的集合才能计入概率。这里可以通过一次多维前缀和实现。

考虑能否合并 S S S T T T。显然能有贡献的一定是 S ⊂ T S \subset T ST,因而可以继续重复这个高维前缀和,变成 ∑ S ∈ n c n t ′ ( S ) 2 b i t ( S ) c n t ( S ) \displaystyle \sum_{S \in n} \frac{cnt'(S) 2^{{\rm bit}(S)}}{cnt(S)} Sncnt(S)cnt(S)2bit(S) c n t ′ cnt' cnt 表示了 S S S 全部超集的超集个数,是在 c n t cnt cnt 上进一步高维前缀和的结果。此处已经可以暴力计算了。

整体复杂度 O ( n log ⁡ n ) O(n \log n) O(nlogn)

#include <cstdio>
#include <algorithm>
using namespace std;
const long long mod = 998244353ll;
long long power(long long a,long long x)
{
    long long ans = 1;
    while(x)
    {
        if(x&1)
            ans = ans * a % mod;
        a = a * a % mod;
        x >>= 1;
    }
    return ans;
}
long long inv(long long a)
{
    return power(a, mod - 2);
}
long long sum1[15000005], sum2[15000005];
//sum1:高维前缀和。sum2:高维前缀和的高维前缀和
int cal(int x)
{
    int ans = 0;
    while(x)
    {
        ans += (x & 1);
        x >>= 1;
    }
    return ans;
}
int main()
{
    int t, n, m;
    scanf("%d", &t);
    while(t--)
    {
        scanf("%d%d", &n, &m);
        int num = 1 << n;
        for (int i = 0; i < num;i++)
            sum1[i] = sum2[i] = 0;
        for (int i = 1; i <= m; i++)
        {
            int x;
            scanf("%d", &x);
            sum1[x]++;
        }
        for (int j = 0; j < n;j++)
            for (int i = 0; i < num;i++)
                if(i&(1<<j))
                    sum1[i ^ (1 << j)] += sum1[i];
        for (int i = 0; i < num;i++)
            sum2[i] = sum1[i];
        for (int j = 0; j < n;j++)//一定是位数这一位在外
            for (int i = 0; i < num;i++)
                if(i&(1<<j))
                    sum2[i ^ (1 << j)] += sum2[i];
        long long ans = 0;
        for (int i = 0; i < num;i++)
        {
            long long now = 1 << cal(i);
            now = now * inv(sum1[i]) % mod;
            now = now * sum2[i] % mod;
            ans = (ans + now) % mod;
        }
        printf("%lld\n", ans);
    }
    return 0;
}

I Array

题意:给定一个长度为 n n n 的序列 { a n } \{ a_n \} {an},问其中满足区间 [ l , r ] [l,r] [l,r] 上的众数出现次数多于 r − l + 1 2 \displaystyle \frac{r-l+1}{2} 2rl+1 的区间个数。 n ≤ 1 × 1 0 6 n \leq 1\times 10^6 n1×106

解法:此题为洛谷原题P4062 [Code+#1]Yazid 的新生舞会

首先,力扣上曾经涉及过这一想法面试题 17.10. 主要元素。考虑如何空间复杂度 O ( 1 ) O(1) O(1) 的找到数组中出现次数过半的元素(下称主要元素)。如果待查元素出现计数器加一,否则减一。如果遍历完整个数组,计数器大于 0 0 0,则一定过半。此法为 Boyer-Moore 投票算法。

考虑此题。枚举每一个可能成为主要元素的数,然后去查询哪些区间时以这个数为主要元素的。根据数据规模,很容易发现,只能是 O ( n ) O(n) O(n) 或者 O ( n log ⁡ n ) O(n \log n) O(nlogn) 的做法。本文考虑 O ( n ) O(n) O(n) 的做法,即枚举每一种类的数并且遍历一次所有它出现的位置。下面考虑主要元素为 x x x 的情况。

根据 Boyer-Moore 投票算法,我们容易发现,对应于原数列,可以列出一个前缀和数组 s u m sum sum

s u m i = { s u m i − 1 + 1 , a i = x s u m i − 1 − 1 , a i ≠ x sum_i= \begin{cases} sum_{i-1}+1, & a_i=x \\ sum_{i-1}-1, & a_i \neq x \end{cases} sumi={sumi1+1,sumi11,ai=xai=x

那么对于两个 x x x 中间 [ l + 1 , r − 1 ] [l+1,r-1] [l+1,r1] a l = a r = x a_l=a_r=x al=ar=x), s u m i sum_i sumi 是一个以 − 1 -1 1 为公差的等差数列。

接下来考虑答案可能在哪里产生。根据这一投票算法容易发现,只有当 r > l r>l r>l s u m r > s u m l sum_r>sum_l sumr>suml 的时候,才是合法区间。因而就是找到 s u m sum sum 序列上的顺序对个数。

显然,我们不能允许对于每一个数,我们都重新初始化一遍这个 s u m sum sum 数组然后使用 O ( n ) O(n) O(n) 或者 O ( n log ⁡ n ) O(n \log n) O(nlogn) 的复杂度求顺序对数,因而还是考虑局部调整。这是本次比赛中第二次使用到局部调整这一方法的题目。

我们考虑记录每一个前缀和出现了多少次,记录在一个数组或者 map 中。显然不允许我们直接将每一个可能值全塞进去,我们可以将这一轮新产生的值的两端计入(等效于记录出现次数的差分数组)。维护一个单一变量 c n t cnt cnt 表示其某一个前缀和,此处维护的究竟是哪一个前缀和是根据我们的需要来进行调整的。再记录一个变量 t o t tot tot 表示合法的 c n t cnt cnt 的和。这里才是我们要的值。

具体的操作如下:

首先记录一个 s u m sum sum 中出现的最小值 m i n i m u m minimum minimum。然后对于第一个出现 x x x 前的进行统计,完成 c n t cnt cnt 数组与 t o t tot tot 数组统计,此处均为 1 1 1

我们遍历到了一个 x x x 出现的位置,记它是第 y y y 个出现的 x x x,且 a z = x a_z=x az=x。那么当前的 s u m sum sum 等于 − z + 2 y -z+2y z+2y。将 c n t cnt cnt 移动到这一位置(其实这一操作是在上一阶段完成的)。由于 t o t tot tot 中已经了计入比 − z + 2 y − 1 -z+2y-1 z+2y1 小的全部 c n t cnt cnt 了,加上这一次的,就可以统计入答案 a n s ans ans 了。这里表示以 z z z 为右端点的答案。并且这个 − z + 2 y -z+2y z+2y 值出现了,要计入 s u m sum sum 数组的次数统计中,用差分两步操作实现。

接下来考虑后面的一些值做右端点。记下一次出现 x x x 的位置为 z ′ z' z,则在 [ z , z ′ ] [z,z'] [z,z] 区间中会连续下降。当当前的 s u m sum sum 值还未低于 m i n i m u m minimum minimum 的时候,那么证明低于当前 s u m sum sum 值的前缀是存在的,存在于 t o t tot tot 中。计入答案,然后由于当前的 s u m sum sum 的下降, t o t tot tot 中要挖去这一个 s u m sum sum 对应的次数,传导下来 c n t cnt cnt 与次数的差分数组都要相应的修改,直到走到下一个值或者直接跌穿 m i n i m u m minimum minimum。如果跌穿了,那么 c n t cnt cnt t o t tot tot 已经没有值了——因为没有比现在的 s u m sum sum 还要小的值了。同时更新次数差分数组与 m i n i m u m minimum minimum

容易发现,经过这一轮操作后, c n t cnt cnt 表示的已经就是下一轮需要的 s u m sum sum 减一的值了,因而下一轮只需要加上下一位即可, t o t tot tot 同理。

更多的细节可以参看代码。

#include <cstdio>
#include <algorithm>
#include <map>
#include <vector>
using namespace std;
const int N = 1000000;
vector<int> place[N + 5];
int main()
{
    int t = 1, n;
    scanf("%d", &t);
    while(t--)
    {
        int x;
        scanf("%d", &n);
        for (int i = 1; i <= n;i++)
        {
            scanf("%d", &x);
            place[x].push_back(i);
        }
        long long ans = 0;
        for (int num = 0; num <= N; num++)
        {
            if(!place[num].size())
                continue;
            place[num].push_back(n + 1);
            long long cnt = 0, tot = 0;
            //cnt表示了当前sum出现的次数,为次数差分数组dif的前缀和
            map<int, long long> dif;
            //第一个众数数字前的情况,共计 1-place[num][0] 个
            int minimum = 1 - place[num][0], now = 1 - place[num][0];
            dif[1]--;
            dif[now]++;
            //now~0 的数全部出现一次
            for (int i = 0; i + 1 < place[num].size(); i++)
            {
                cnt += dif[now];//加上当前一位的值即可
                tot += cnt;
                ans += tot;
                now++;
                dif[now]++;//当前的sum次数增加1
                dif[now + 1]--;
                int delta = place[num][i + 1] - place[num][i] - 1;
                for (; delta > 0 && now > minimum;delta--)
                {
                    tot -= cnt;//先把当前的sum对应次数挖掉
                    ans += tot;
                    cnt -= dif[now - 1];//连带修改
                    now--;
                    dif[now]++;
                    dif[now + 1]--;
                }
                if(delta>0)//跌穿,直接重来
                {
                    dif[minimum]--;
                    minimum -= delta;
                    dif[minimum]++;
                    now = minimum;
                    cnt = 0;
                    tot = 0;
                }
            }
        }
        printf("%lld\n", ans);
        for (int i = 0; i <= N; i++)
            place[i].clear();
    }
    return 0;
}

L Penguin Love Tour

题意:给定一棵树,点上有权值 a i a_i ai,边上有边权 w i w_i wi,现在可以对于每一个点,让其连接的一条边的权值下降 a i a_i ai,问经过这种操作后树的直径最小值。

解法:树上直径可以认为是最大值,最大值最小——二分答案。因而二分出可能直径,然后贪心或者 DP 去考虑如何减边权。

注意到一个点只能用一次,而且一定会用一次,那么就会分“用给子树”与“用给父亲”这两种情况。对这两种情况进行转移,记用给父亲的子树到当前节点最长链 f u , 0 f_{u,0} fu,0,用给子树某一条边是 f u , 1 f_{u,1} fu,1

首先考虑 u u u 把权值用给父亲的情况,更新 f u , 0 f_{u,0} fu,0。当我们访问到 u u u 的某一个儿子 v v v 的时候,如果当前父亲没有给子树任何一条边减权值出来的最长链(直接统计出来的最长链)加上当前这个儿子转移而来的最长链的总和(即这个子树内的直径)都没有超出要求,那么父亲也没必要给这个儿子减权值,同时这个儿子贡献上来的链就可以计入 f u , 0 f_{u,0} fu,0。否则,父亲不给子树中一条边减权值就已经不行了, f u , 0 = ∞ f_{u,0}=\infty fu,0=

考虑父亲给子树减了权值的情况,即统计 f u , 1 f_{u,1} fu,1,分以下两种情况——给当前的 v v v 减权值与不给 v v v 减但是给子树中其他的减了。这时,其他的子树贡献上来的最长链长度在两种情况下分别为 f u , 0 f_{u,0} fu,0 f u , 1 f_{u,1} fu,1

首先考虑第一种情况。如果当前父亲给别人打折但没给 v v v 打折,结果当前的 u → v u \to v uv 仅由儿子就能直接解决(即二者之和小于等于要求值),那么父亲可以给 v v v 打折也可以不打,这两种子情况都计入 f u , 1 f_{u,1} fu,1。如果必须父亲出面给 v v v 打折才能满足条件,那么只有父亲给 v v v 打折这一种情况能计入。

第二种情况就是父亲必须给子树打折才能满足。那么这个时候 u → v u \to v uv 只能靠儿子了,儿子打折成了最好,不能成就没办法了只能记 f u , 1 = ∞ f_{u,1}=\infty fu,1=

最后统计根节点的情况,若 f u , 0 f_{u,0} fu,0 或者 f u , 1 f{u,1} fu,1 有一种不为 ∞ \infty 即能成立,对应于 1 1 1 给子树打折了或者完全没打折。

整体复杂度 O ( n log ⁡ N ) O(n \log N) O(nlogN)。代码中有详细的注释。

#include <cstdio>
#include <algorithm>
using namespace std;
const long long inf = 0x3f3f3f3f3f3f3f3fll;
struct line
{
    int from;
    int to;
    long long w;
    int next;
};
struct line que[200005];
int cnt, headers[100005];
void add(int from,int to,long long w)
{
    cnt++;
    que[cnt].from = from;
    que[cnt].to = to;
    que[cnt].w = w;
    que[cnt].next = headers[from];
    headers[from] = cnt;
}
long long f[100005][2];
long long a[100005];
long long ask;
int n;
void dfs(int now,int pre)
{
    f[now][0] = f[now][1] = 0;
    for (int i = headers[now]; i;i=que[i].next)
    {
        long long to = que[i].to, w = que[i].w;
        if (to == pre)
            continue;
        dfs(to, now);
        //dp[now][0] 表示父亲没有给子树中任何一条边打折,最长的链。dp[now][1] 表示父亲给其中某一条边打过折,最长的链。显然,dp[now][0]>=dp[now][1]
        //0 给父亲(子树中未使用),1给子树
        long long with_father_son_min = min(dp[to][1] + max(0, w - a[now]), dp[to][0] + max(0, w - a[to] - a[now]));
        long long no_father_son_min = min(dp[to][1] + w, dp[to][0] + max(0, w - a[to]));
        if (dp[now][0] + with_father_son_min <= mid)
        //父亲没打过折+父亲给当前边打折<=mid
        {
            if (dp[now][1] + no_father_son_min <= mid)
            //父亲给别的边打折了,这条边父亲不给打折,由儿子出马打折<=mid
                dp[now][1] = max(dp[now][1], min(max(dp[now][0], with_father_son_min), no_father_son_min));
                //此时这条边儿子可以打折,儿子也可以不打折 min(儿子打折,儿子不打折)。父亲由于给其他边打折了,那么此时这个链计入父亲打过折的部分。
                //max(dp[now][0], min(dp[to][1] + max(0, w - a[now]), dp[to][0] + max(0, w - a[to] - a[now]))):后半部分是父亲没有参与打折的情况,因而可以计入父亲完全不给子树打折的最长链情况。
            else
            //父亲不给打折就超过了,必须让儿子出马打折
                dp[now][1] = max(dp[now][0], with_father_son_min);
                //儿子必须打折,此时计入父亲给别的边打折了之后的最长边。同时,父亲没有给这条边打折,那么这条链也可以认定为父亲没有给子树打折的情况。
        }
        else
        //父亲之前没打过折,父亲给当前边打折都超过了
            if (dp[now][1] + no_father_son_min <= mid)
            //父亲给别的边打折了,当前边由儿子出马打折,父亲给别的打折的边的最长+儿子打折这条边没超过
                dp[now][1] = max(dp[now][1], no_father_son_min);
                //允许由儿子出面打折,计入父亲给打过折情况的最长边
            else
                dp[now][1] = MAX;//父亲打过折的最长链+儿子打折了也没用
        /*--------以上为讨论父亲打折后的最长边情况---------*/
        if (dp[now][0] + no_father_son_min <= mid)
        //父亲没有打过一次折的最长边+儿子给当前这条边打折或者不打折的最长边 小于mid
            dp[now][0] = max(dp[now][0], no_father_son_min);
            //儿子的最长链计入父亲完全不给子树打折的情况
        else
            dp[now][0] = MAX;
            //直接超过了,那么这种情况炸掉,inf,证明父亲必须给子树打折
        /*--------以上为讨论父亲不打折的最长边情况---------*/
    }
}
int main()
{
    int t;
    scanf("%d", &t);
    while(t--)
    {
        scanf("%d", &n);
        cnt = 0;
        for (int i = 1; i <= n;i++)
            headers[i] = 0;
        for (int i = 1; i <= n; i++)
            scanf("%lld", &a[i]);
        long long left = 0, right = 0;
        for (int i = 1; i < n; i++)
        {
            int u, v;
            long long x;
            scanf("%d%d%lld", &u, &v, &x);
            right += x;
            add(u, v, x);
            add(v, u, x);
        }
        long long ans = 0;
        while(left<=right)
        {
            long long mid = (left + right) >> 1;
            ask = mid;
            dfs(1, 1);
            if(f[1][0]<=mid || f[1][1]<=mid)
            {
                ans = mid;
                right = mid - 1;
            }
            else
                left = mid + 1;
        }
        printf("%lld\n", ans);
    }
    return 0;
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值