2022 年杭电多校第六场补题记录

A Multiply 2 Divide 2

题意:给定长度为 n n n 的序列 { a n } \{a_n\} {an},一次操作可以将 a i ← ⌊ a i 2 ⌋ a_i \leftarrow \left \lfloor \dfrac{a_i}{2}\right \rfloor ai2ai a i ← 2 a i a_i \leftarrow 2a_i ai2ai i ∈ [ 1 , n ] i \in [1,n] i[1,n],问最少操作数使得 { a i } \{a_i\} {ai} 递增。 n ≤ 1 × 1 0 5 n \leq 1\times 10^5 n1×105 a i ≤ 1 × 1 0 5 a_i \leq 1\times 10^5 ai1×105

解法:首先考虑一个朴素 dp: f i , j , k f_{i,j,k} fi,j,k 表示第 i i i 个数字除了 j j j 次,乘了 k k k 次之后,使得前面不递减的最小方案。那么转移是显然的: f i + 1 , j ′ , k ′ ← f i , j , k + j ′ + k ′ f_{i+1,j',k'} \leftarrow f_{i,j,k}+j'+k' fi+1,j,kfi,j,k+j+k,当且仅当 ⌊ a i + 1 2 j ′ ⌋ 2 k ′ ≥ ⌊ a i 2 j ⌋ 2 k \left \lfloor \dfrac{a_{i+1}}{2^{j'}}\right \rfloor 2^{k'} \geq \left \lfloor \dfrac{a_i}{2^j}\right \rfloor 2^{k} 2jai+12k2jai2k。但是且不论如何优化转移,第二维 j j j 的数目为 log ⁡ m \log m logm,第三维的数目可以到 log ⁡ n log ⁡ m \log n \log m lognlogm 级别,显然状态数就有 O ( n log ⁡ 2 m log ⁡ n ) O(n \log^2 m \log n) O(nlog2mlogn),不可接受。

考虑如何优化这一转移式。设 M = max ⁡ 1 ≤ i ≤ n { a i } \displaystyle M =\max_{1 \leq i \leq n} \{a_i\} M=1inmax{ai},首先容易注意到一个性质:一个数字要处理一定是先除后乘;如果 a i a_i ai 的最终处理结果大于 M M M,那么最后一步一定是乘法。因而若 ⌊ a i + 1 2 j ⌋ 2 k > M \left \lfloor \dfrac{a_{i+1}}{2^{j}}\right \rfloor 2^{k} > M 2jai+12k>M 且存在 k 1 k_1 k1 使得 k 1 < k k_1k1<k ⌊ a i + 1 2 j ⌋ 2 k ′ > M \left \lfloor \dfrac{a_{i+1}}{2^{j}}\right \rfloor 2^{k'} > M 2jai+12k>M,则为了让 j , j > i j,j>i j,j>i 的数字经过处理后都大于等于 ⌊ a i + 1 2 j ⌋ 2 k \left \lfloor \dfrac{a_{i+1}}{2^{j}}\right \rfloor 2^{k} 2jai+12k,所需要付出的代价为 g i , k = g i , k ′ + ( n − i ) ( k − k ′ ) g_{i,k}=g_{i,k'}+(n-i)(k-k') gi,k=gi,k+(ni)(kk)

简单来说,就是 a i a_i ai 为了大于等于前面的数字,多乘了几次 2 2 2,那么后面的转移也需要多乘这么多个 2 2 2,而不会改变最优解中后面这些数字除法的次数。因而如果单纯考虑后面的转移,那么这些多除的 2 2 2 就都可以直接消掉不予考虑。基于这样的考虑,我们可以首先预处理出 g i , j g_{i,j} gi,j 数组,表示第 i i i 个数字首先除了 j j j 次,然后乘到恰好大于 M M M,以这样的 a i ′ a_i' ai 作为基准,使得后面的数字都单调不递减所需要的最少操作数。如果乘的次数再多一点,对于后面的数字来说只是乘的次数更多,不会影响到后面这些数字前面除法的次数,因而总次数也就只是单纯的加上 n − i n-i ni 乘以 i i i 这一位多乘的次数,这部分可以利用 g i , j g_{i,j} gi,j 预处理计算;如果最终乘的次数更少,不足 M M M,那么乘法次数不足 log ⁡ m \log m logm 次,这样的 k ≤ log ⁡ m k \leq \log m klogm,可以压入 dp 数组直接进行上述的 dp。因而通过这一方法可以将第三维压缩到 log ⁡ m \log m logm,总状态数仅有 O ( n log ⁡ 2 m ) O(n \log ^2m) O(nlog2m)

对于第 i i i 位,首先预处理出其进行了 j , 0 ≤ j ≤ log ⁡ a i j,0 \leq j \leq \log a_i j,0jlogai 次除法的数组 { b } \{b\} {b},之后的乘法均是以 { b } \{b\} {b} 数组作为基底往上乘。对于 g i , j g_{i,j} gi,j 的转移,倒序进行,每次只会让后面的数字全乘以 2 2 2 或者不乘。

对于 f f f 的转移,当最终计算得到的 a a a 不超过 M M M 时,由于状态数过多,因而考虑使用前缀和优化: f i , j , k ← min ⁡ k ′ { f i − 1 , j ′ k ′ + j + k } , ⌊ a i 2 j ⌋ 2 k ≥ ⌊ a i − 1 2 j ′ ⌋ 2 k ′ \displaystyle f_{i,j,k} \leftarrow \min_{k'}\{f_{i-1,j'k'}+j+k\},\left \lfloor \dfrac{a_i}{2^{j}}\right \rfloor 2^{k} \geq \left \lfloor \dfrac{a_{i-1}}{2^{j'}}\right \rfloor 2^{k'} fi,j,kkmin{fi1,jk+j+k},2jai2k2jai12k。在实际处理的时候,对于同一个 i i i,一个 ( j , k ) (j,k) (j,k) 状态唯一表示了 i i i 预处理出的 b b b 数组中的一个值,因而可以进行前缀和优化,具体见代码。若 a i a_i ai 已经超过 M M M,则直接使用 g i , j g_{i,j} gi,j 即可。

总的时间复杂度 O ( n log ⁡ 2 m ) \mathcal O(n \log^2m) O(nlog2m)

#include 
using namespace std;
const long long inf = 0x3f3f3f3f3f3f3f3fll;
struct node
{
    long long val;
    int times;
    node(long long _val, int _times)
    {
        val = _val;
        times = _times;
    }
};
int main()
{
    int t, n;
    scanf("%d", &t);
    while(t--)
    {
        scanf("%d", &n);
        vector<int> a(n + 1);
        vector<vector<node>> trans(n + 1);
        vector<vector<long long>> g(n + 1, vector<long long>(20, inf)), data(n + 1, vector<long long>(20, 0));
        int maximum = 0;
        for (int i = 1; i <= n;i++)
        {
            scanf("%d", &a[i]);
            maximum = max(maximum, a[i]);
        }
        for (int i = 1; i <= n;i++)
        {
            vector<int> base;
            base.push_back(a[i]);
            while (base.back() > 1)
                base.push_back(base.back() / 2);
            int maxgroup = base.size();
            trans[i].emplace_back(0, maxgroup);
            for (int j = maxgroup - 1; base.back() <= maximum;j--)
                for (int k = maxgroup - 1; k >= j && k >= 0;k--)
                {
                    if (base[k] <= maximum)
                    {
                        trans[i].emplace_back(base[k], 2 * k - j);
                        base[k] *= 2;
                    }
                    if (base[k] > maximum && !data[i][k])
                    {
                        data[i][k] = base[k];
                        g[i][k] = 2 * k - j + 1;
                    }
                }
        }
        for (int i = n - 1; i >= 1;i--)
            for (int j = 0; j < 20 && data[i][j]; j++)
            {
                long long now = inf;
                for (int k = 0; k < 20 && data[i + 1][k]; k++)
                    if(data[i + 1][k] < data[i][j])
                        now = min(now, g[i + 1][k] + n - i);
                    else
                        now = min(now, g[i + 1][k]);
                g[i][j] += now;
            }
        long long ans = inf;
        for (int i = 2; i <= n; i++)
        {
			int k = 0;
			int minimum = trans[i - 1][0].times;
            for (int j = 0; j < trans[i].size();j++)
            {
                while (k + 1 < trans[i - 1].size() && trans[i - 1][k + 1].val <= trans[i][j].val)
                {
                    minimum = min(minimum, trans[i - 1][k + 1].times);
                    k++;
                }
                trans[i][j].times += minimum;
            }
            while (k + 1 < trans[i - 1].size() && trans[i - 1][k + 1].val <= maximum)
            {
                minimum = min(minimum, trans[i - 1][k + 1].times);
                k++;
            }
            for (int j = 0; j < 20 && data[i][j]; j++)
                ans = min(1ll * ans, minimum + g[i][j]);
        }
        for (auto i : trans[n])
            ans = min(ans, (long long)i.times);
        printf("%lld\n", ans);
    }
    return 0;
}

B Hack of Multiply 2 Divide 2

题意:构造长度不超过 n n n 的序列 { a n } \{a_n\} {an},一次操作可以将 a i ← ⌊ a i 2 ⌋ a_i \leftarrow \left \lfloor \dfrac{a_i}{2}\right \rfloor ai2ai a i ← 2 a i a_i \leftarrow 2a_i ai2ai i ∈ [ 1 , n ] i \in [1,n] i[1,n],在最少操作数下使得 { a i } \{a_i\} {ai} 递增,使得 a n ≥ 2 128 a_n \geq 2^{128} an2128 n ≤ 1 × 1 0 5 n \leq 1\times 10^5 n1×105 a i ≤ 1 × 1 0 5 a_i \leq 1\times 10^5 ai1×105

解法:(思想来源于题解)

设序列最大值为 M M M,考虑最终序列(必然为不递增排列)中最高位为 2 i 2^i 2i 的数字个数为 c i c_i ci,则对于对于序列中最后一步需要乘的数字(即 i ≥ ⌈ log ⁡ 2 M ⌉ i \geq \left \lceil \log_2M\right \rceil ilog2M ∑ j = i c j \displaystyle \sum_{j=i} c_j j=icj 个数字),必然有 2 ⌈ log ⁡ 2 M ⌉ c i > ∑ j = i + 1 c j \displaystyle 2 \left \lceil \log_2 M \right \rceil c_i>\sum_{j=i+1} c_j 2log2Mci>j=i+1cj。这是由于对于一个最高位为 2 i 2^i 2i 的数字 x x x,可以花费 2 ⌈ log ⁡ 2 M ⌉ 2 \left \lceil \log_2 M \right \rceil 2log2M 次(实际次数还要少于这个数字,只是一个上界)将其先除到 1 1 1 再乘到 2 i 2^i 2i,这样对于后面所有 j , j > i j,j>i j,j>i 的数字,均可以少乘一次(因为对它们的最后一次操作必然为乘法)。这样操作对总次数的变化量为 2 ⌈ log ⁡ 2 M ⌉ c i − ∑ j = i + 1 c j \displaystyle 2 \left \lceil \log_2 M \right \rceil c_i-\sum_{j=i+1} c_j 2log2Mcij=i+1cj

基于这一想法,一个可行的构造是 i 2 k , i 2 k , ⋯   , i 2 k ⏟ d i , ( i − 1 ) 2 k , ( i − 1 ) 2 k , ⋯   , ( i − 1 ) 2 k ⏟ d i − 1 , ⋯   , 2 k , 2 k , ⋯   , 2 k ⏟ d 1 \underbrace{i2^k,i2^k,\cdots,i2^k}_{d_i},\underbrace{(i-1)2^k,(i-1)2^k,\cdots,(i-1)2^k}_{d_{i-1}},\cdots,\underbrace{2^k,2^k,\cdots,2^k}_{d_1} di i2k,i2k,,i2k,di1 (i1)2k,(i1)2k,,(i1)2k,,d1 2k,2k,,2k,其中 k k k 为一常数,可以取 k = 9 k=9 k=9,这是为了防止每个数字的 l o w b i t \rm lowbit lowbit 过小,使得 a i a_i ai 修改成 2 k 2^k 2k 过于容易。从小到大对 d i d_i di 进行 dp,令 c 1 = 1 c_1=1 c1=1,后面的 c i c_i ci 需要满足 c i ( 2 log ⁡ 2 l o w b i t ( 512 i ) + 2 ) > ∑ j = 1 i − 1 c j \displaystyle c_i(2 \log_2{\rm lowbit}(512i)+2)>\sum_{j=1}^{i-1}c_j ci(2log2lowbit(512i)+2)>j=1i1cj。此外,为了让最大的数字尽可能大,序列也需要尽可能的长,因而最大的 i i i 可取 ⌊ log ⁡ 2 M 2 k ⌋ \left \lfloor \log_2\dfrac{M}{2^k} \right \rfloor log22kM

#include 
using namespace std;
int lowbit(int x)
{
    return x & (-x);
}
int main()
{
    vector<int> sum;
    sum.push_back(0);
    for (int i = 1; i <= 1e5 / 512; i++)
    {
        int div = 2 * __lg(512 * lowbit(i)) + 2;
        int now = sum.back() / div + 1;
        sum.push_back(sum.back() + now);
    }
    printf("%d\n", sum.back());
    for (int i = sum.size() - 1; i >= 0;i--)
        for (int j = 1; j <= sum[i] - sum[i - 1];j++)
            printf("%d ", i * 512);
    return 0;
}

C Find the Number of Paths

题意:有 n + k n+k n+k 个城市,对于 i ∈ [ 1 , n + k − 1 ] i \in [1,n+k-1] i[1,n+k1],从第 i i i 个城市到第 i + 1 i+1 i+1 个城市有 n + k − i n+k-i n+ki 条单向道路;对于 i ∈ [ n , n + k ] i \in [n,n+k] i[n,n+k],第 i i i 个城市向第 i − j i-j ij 个城市有 a j a_j aj 条单向边。给定长度为 n − 1 n-1 n1 的序列 { a i } \{a_i\} {ai},问从第 k + 1 k+1 k+1 个城市出发,走到第 i i i 个城市且恰好走了 k k k 条路的方案数,需要对 i ∈ [ k + 1 , n + k ] i \in [k+1,n+k] i[k+1,n+k] 求出。 n , k ≤ 2 × 1 0 5 n,k \leq 2\times 10^5 n,k2×105

解法:首先对序号重编号: i ← n + k − i i \leftarrow n+k-i in+ki,则原问题为:

n + k n+k n+k 个城市,对于 i ∈ [ 0 , n + k − 2 ] i \in [0,n+k-2] i[0,n+k2],从第 i + 1 i+1 i+1 个城市到第 i i i 个城市有 i i i 条单向道路;对于 i ∈ [ 0 , n + k − 1 − j ] , j ∈ [ 1 , n − 1 ] i \in [0,n+k-1-j],j \in [1, n-1] i[0,n+k1j],j[1,n1],第 i i i 个城市向第 i + j i+j i+j 个城市有 a j a_j aj 条单向边。给定长度为 n − 1 n-1 n1 的序列 { a i } \{a_i\} {ai},问从第 n − 1 n-1 n1 个城市出发,走到第 i i i 个城市且恰好走了 k k k 条路的方案数,需要对 i ∈ [ 0 , n − 1 ] i \in [0,n-1] i[0,n1] 求出。

这样做的好处是方便进行后续操作[1][2]。首先令 f i , j f_{i,j} fi,j 为从 n − 1 n-1 n1 出发到第 j j j 个城市,共走了 i i i 步的方案数。其转移是显然的: f i + 1 , j ← f i , j + 1 ( j + 1 ) + ∑ k = 0 j f i , k a j − k \displaystyle f_{i+1,j} \leftarrow f_{i,j+1}(j+1)+\sum_{k=0}^{j}f_{i,k}a_{j-k} fi+1,jfi,j+1(j+1)+k=0jfi,kajk,其中对于 ∀ i ≥ n \forall i \geq n in a i = 0 a_i=0 ai=0

首先考虑生成函数化,优化单步递推。令 F i ( x ) = ∑ j = 0 n + k − 1 f i , j x j \displaystyle F_{i}(x)=\sum_{j=0}^{n+k-1} f_{i,j}x^j Fi(x)=j=0n+k1fi,jxj g ( x ) = ∑ i = 0 n + k − 1 a i x i \displaystyle g(x)=\sum_{i=0}^{n+k-1}a_ix^i g(x)=i=0n+k1aixi,则 f i , j + 1 ( j + 1 ) f_{i,j+1}(j+1) fi,j+1(j+1) 可以视为求导[1]; ∑ k = 0 j f i , k a j − k \displaystyle \sum_{k=0}^jf_{i,k}a_{j-k} k=0jfi,kajk 为一卷积形式,可写作 F i ( x ) g ( x ) F_i(x)g(x) Fi(x)g(x)[2]。因而 F i + 1 ( x ) = F i ′ ( x ) + F i ( x ) g ( x ) F_{i+1}(x)=F'_i(x)+F_{i}(x)g(x) Fi+1(x)=Fi(x)+Fi(x)g(x)。重新编号之后求导变得方便——零次项不能转移直接通过求导被消除了;卷积形式也变得清晰起来。初值 F 0 ( x ) = x n − 1 F_0(x)=x^{n-1} F0(x)=xn1

但是仅利用这一生成函数无法进行 k k k 次暴力运算,因而需要更快的方法。看到原函数和导函数在一起,可以联想到利用形如 e f ( x ) e^{f(x)} ef(x) 进行配凑: ( A ( x ) e f ( x ) ) ′ = e f ( x ) ( A ( x ) + A ′ ( x ) f ′ ( x ) ) ) (A(x)e^{f(x)})'=e^{f(x)}(A(x)+A'(x)f'(x))) (A(x)ef(x))=ef(x)(A(x)+A(x)f(x)))。因而考虑 G i ( x ) = F i ( x ) e ∫ g ( x ) d x G_{i}(x)=F_{i}(x)e^{\int g(x) {\rm d}x} Gi(x)=Fi(x)eg(x)dx,则有 G i + 1 ( x ) = e ∫ g ( x ) d x ( F i ′ ( x ) + F i ( x ) g ( x ) ) = G i ′ ( x ) G_{i+1}(x)=e^{\int g(x){\rm d}x}(F'_i(x)+F_i(x)g(x))=G_{i}'(x) Gi+1(x)=eg(x)dx(Fi(x)+Fi(x)g(x))=Gi(x)。因而 F k ( x ) = G 0 ( k ) ( x ) e ∫ g ( x ) d x F_k(x)=\dfrac{G_0^{(k)}(x)}{e^{\int g(x){\rm d}x}} Fk(x)=eg(x)dxG0(k)(x)。预处理 e ∫ g ( x ) d x e^{\int g(x) {\rm d}x} eg(x)dx 的泰勒展开前 k k k 项,再进行 O ( n ) \mathcal O(n) O(n) 的求导(第 m , m ≥ k m,m \geq k m,mk a m x m a_mx^m amxm k k k 次导数为 a m m ! ( m − k ) ! x m − k \dfrac{a_mm!}{(m-k)!}x^{m-k} (mk)!amm!xmk),最后取多项式的逆即可。总时间复杂度 O ( n log ⁡ n ) \mathcal O(n \log n) O(nlogn)

int main()
{
    int t, n, k;
    scanf("%d", &t);
    while(t--)
    {
        scanf("%d%d", &n, &k);
        Poly A(n, 0);
        for (int i = 1; i < n;i++)
            scanf("%d", &A[i]);
        Poly g = (A.integ()).exp(k + 1);
        Poly p(k + n, 0);
        for (int i = 0; i <= k;i++)
            p[i + n - 1] = g[i];
        Poly pk(n);
        for (int i = 0; i < n;i++)
            pk[i] = 1ll * fac[i + k] * ifac[i] % P * p[i + k] % P;
        Poly f = pk * g.inv(n);
        for (int i = 0; i < n;i++)
            printf("%d%c", f[n - i - 1], " \n"[i == n - 1]);
    }
    return 0;
}

F Maex

题意:给定一个 n n n 个点的树,树上每个点给一个互不相同的权值 v i v_i vi,对于点 u u u 对树权值的贡献为 m e x w ∈ s u b t r e e u { v w } {\rm mex}_{w \in {\rm subtree}_u} \{v_w\} mexwsubtreeu{vw},问树最大权值。 n ≤ 1 × 1 0 5 n \leq 1\times 10^5 n1×105

解法:显然树上有非零权值的仅为一条链。每个点的 m e x \rm mex mex 最大为 s i z u {\rm siz}_u sizu,因而统计子树大小然后从上往下去搜索每个叶子即可。总复杂度 O ( n ) \mathcal O(n) O(n)

#include 
using namespace std;
const int N = 500000;
vector<int> graph[N + 5];
int siz[N + 5];
int main()
{
    int t, n;
    scanf("%d", &t);
    while(t--)
    {
        scanf("%d", &n);
        vector<vector<int>> graph(n + 1);
        for (int i = 1, u, v; i < n;i++)
        {
            scanf("%d%d", &u, &v);
            graph[u].push_back(v);
            graph[v].push_back(u);
        }
        long long ans = 0;
        vector<int> siz(n + 1, 0);
        function<void(int, int)> dfs1 = [&](int place, int father)
        {
            siz[place] = 1;
            for (auto i : graph[place])
                if (i != father)
                {
                    dfs1(i, place);
                    siz[place] += siz[i];
                }
        };
        dfs1(1, 1);
        function<void(int, int, long long)> dfs2 = [&](int place, int father, long long now)
        {
            ans = max(ans, now);
            for (auto i : graph[place])
                if (i != father)
                    dfs2(i, place, now + siz[i]);
        };
        dfs2(1, 1, n);
        printf("%lld\n", ans);
    }
    return 0;
}

G Shinobu loves trip

题意:给定质数 P P P 的乘法群 G G G,给定 n n n 个子集 S i = { x ∣ x = s i a j   m o d   P , j ∈ [ 1 , d i ] } S_i=\{x|x=s_ia^{j} \bmod P,j \in [1,d_i]\} Si={xx=siajmodP,j[1,di]} q q q 次询问 x i x_i xi 被几个集合包含。 n , q ≤ 1 × 1 0 3 n,q \leq 1\times 10^3 n,q1×103 P ≤ 1 × 1 0 9 + 7 P \leq 1\times 10^9+7 P1×109+7 d i ≤ 2 × 1 0 5 d_i \leq 2\times 10^5 di2×105

解法:将每个集合写作 S i = { x ∣ x = a j   m o d   P , j ∈ [ 1 , d i ] } S_i=\{x|x=a^j \bmod P,j \in [1,d_i]\} Si={xx=ajmodP,j[1,di]},则每次查询为 x i s j , j ∈ [ 1 , n ] \dfrac{x_i}{s_j},j \in [1,n] sjxi,j[1,n]。那么只需要对 { a x } \{a^x\} {ax} 集合查询即可。

H Shinobu Loves Segment Tree

题意:给定下面代码:

void build(int id, int l, int r){
  	value[id] += r-l+1;
  	if(l == r) return;
  	int mid = (r+l)/2;
  	build(id*2, l, mid);
  	build(id*2+1, mid+1, r);
  	return;
}

调用 build(1,1,i),其中 i ∈ [ 1 , n ] i \in [1,n] i[1,n] T T T 次查询 val[x]的值。 n , x ≤ 1 × 1 0 9 n,x \leq 1\times 10^9 n,x1×109 T ≤ 1 × 1 0 5 T \leq 1\times 10^5 T1×105

解法:对于 1 1 1 号节点,其 val的增加量为 [ 1 , 2 , 3 , ⋯   , n ] [1,2,3,\cdots,n] [1,2,3,,n]。对于其左侧子树,其增加量变化为 [ 0 , 1 , 1 , 2 , 2 , 3 , 3 , ⋯   , ⌊ n 2 ⌋ − 1 , ⌊ n 2 ⌋ − 1 , ⌊ n 2 ⌋ ] \left[0,1,1,2,2,3,3,\cdots,\left \lfloor \dfrac{n}{2}\right \rfloor-1,\left \lfloor \dfrac{n}{2}\right \rfloor-1,\left \lfloor \dfrac{n}{2}\right \rfloor \right] [0,1,1,2,2,3,3,,2n1,2n1,2n](若 n n n 为偶数则 ⌊ n 2 ⌋ \left \lfloor \dfrac{n}{2}\right \rfloor 2n 有两个),右侧子树为 [ 0 , 1 , 2 , 2 , 3 , 3 , ⋯   , ⌊ n 2 ⌋ − 1 , ⌊ n 2 ⌋ − 1 , ⌊ n 2 ⌋ ] \left[0,1,2,2,3,3,\cdots,\left \lfloor \dfrac{n}{2}\right \rfloor-1,\left \lfloor \dfrac{n}{2}\right \rfloor-1,\left \lfloor \dfrac{n}{2}\right \rfloor \right] [0,1,2,2,3,3,,2n1,2n1,2n]。即左侧子树为下取整,右侧子树为上取整。

不难得到规律:最终的增加序列一定形如: [ 0 , 0 , ⋯   , 0 , 1 , 1 , ⋯   , 1 , 2 , 2 , ⋯   , 2 ⏟ 2 w 个 , 3 , 3 , ⋯   , 3 ⏟ 2 w 个 , ⋯ k − 1 , k − 1 , ⋯   , k − 1 ⏟ 2 w 个 , k , k , ⋯   , k ] [0,0,\cdots,0,1,1,\cdots,1,\underbrace{2,2,\cdots,2}_{2^w 个},\underbrace{3,3,\cdots,3}_{2^w 个},\cdots \underbrace{k-1,k-1,\cdots,k-1}_{2^w 个},k,k,\cdots,k] [0,0,,0,1,1,,1,2w 2,2,,2,2w 3,3,,3,2w k1,k1,,k1,k,k,,k]。这是由于对于非 1 1 1 和最大值 k k k 的个数必然是增加一倍,无论是下取整还是上取整。而最后 val增加的值对于 build(1,1,i)有单调性—— i i i 越大 val增加的越多。因而使用二分答案计算出 1 1 1 的数目和 k k k 的数目即可。 k k k 可以直接通过模拟最后一次操作得到, w w w 为从线段树上根节点走到 x x x 的步数。总时间复杂度 O ( T log ⁡ 2 n ) \mathcal O(T \log ^2n) O(Tlog2n)

#include 
using namespace std;
int main()
{
    int t;
    long long n, x;
    scanf("%d", &t);
    while(t--)
    {
        scanf("%lld%lld", &n, &x);
        if (x == 1)
        {
            printf("%lld\n", n * (n + 1) / 2);
            continue;
        }
        long long orix = x, orin = n;
        vector<int> down;
        while (x > 1)
        {
            down.push_back(x & 1);
            x >>= 1;
        }
        reverse(down.begin(), down.end());
        long long ans = 0, times = 1;
        for (auto i : down)
        {
            times *= 2;
            if (i)
                n >>= 1;
            else
                n = (n + 1) / 2;
        }

        auto check1 = [&](long long now)
        {
            long long place = 1, left = 1, right = now;
            for (auto i : down)
            {
                if (left == right)
                    return false;
                long long mid = (left + right) >> 1;
                if(i)
                {
                    place = place * 2 + 1;
                    left = mid + 1;
                }
                else
                {
                    right = mid;
                    place = place * 2;
                }
            }
            return true;
        };
        long long left = 2, right = orix, st = 1;
        while (left <= right)
        {
            long long mid = (left + right) >> 1;
            if (check1(mid))
            {
                st = mid;
                right = mid - 1;
            }
            else
                left = mid + 1;
        }
        if(st > orin)
        {
            printf("0\n");
            continue;
        }
        auto check2 = [&](long long pos)
        {
            long long place = 1, left = 1, right = pos;
            for (auto i : down)
            {
                long long mid = (left + right) >> 1;
                if(i)
                {
                    left = mid + 1;
                    place = place * 2 + 1;
                }
                else
                {
                    right = mid;
                    place = place * 2;
                }
            }
            return right - left + 1;
        };
        left = st;
        right = orin;
        long long time1 = 0, timemax = 0;
        while (left <= right)
        {
            long long mid = (left + right) >> 1;
            if (check2(mid) <= 1)
            {
                time1 = mid - st + 1;
                left = mid + 1;
            }
            else
                right = mid - 1;
        }
        left = st;
        right = orin;
        while (left <= right)
        {
            long long mid = (left + right) >> 1;
            if (check2(mid) >= n)
            {
                timemax = orin - mid + 1;
                right = mid - 1;
            }
            else
                left = mid + 1;
        }
        long long cenl = 2, cenr = n - 1;
        if (cenl <= cenr)
            ans += (cenr - cenl + 1) * (cenl + cenr) / 2 * times;
        if (n == 1)
            ans += time1;
        else
            ans += time1 + timemax * n;
        printf("%lld\n", ans);
    }
    return 0;
}

I Map

题意:给定平面两相似矩形,保证小矩形包含在大矩形中,问不动点坐标。

解法:设相似比为 k k k,不妨通过旋转平移让大矩形左下顶点处于 ( 0 , 0 ) (0,0) (0,0),小矩形对应左下顶点在 ( x 0 , y 0 ) (x_0,y_0) (x0,y0),小矩形相对于大矩形的旋转角度为 θ \theta θ,设不动点坐标在 ( x , y ) (x,y) (x,y),则小矩形中该点坐标为 ( x 0 + k x cos ⁡ θ − k y sin ⁡ θ , y 0 + k x sin ⁡ θ + k y cos ⁡ θ ) (x_0+k x\cos\theta-ky\sin \theta,y_0+kx\sin \theta+ky\cos \theta) (x0+kxcosθkysinθ,y0+kxsinθ+kycosθ)。因而有方程:
{ ( k cos ⁡ θ − 1 ) x − k sin ⁡ θ y = − x 0 k sin ⁡ θ x + ( k cos ⁡ θ − 1 ) y = − y 0 \left \{ \begin{aligned} (k\cos \theta-1)x-k\sin \theta y=&-x_0\\ k \sin \theta x+(k\cos \theta-1)y=&-y_0\\ \end{aligned} \right . {(kcosθ1)xksinθy=ksinθx+(kcosθ1)y=x0y0
使用克拉默法则进行求解即可。

#include 
using namespace std;
double det(double a, double b, double c, double d)
{
    return a * d - b * c;
}
struct Point
{
    double x, y;
    Point operator-(const Point b)
    {
        return (Point){x - b.x, y - b.y};
    }
    Point operator+(const Point b)
    {
        return (Point){x + b.x, y + b.y};
    }
    Point rotate(double ang)
    {
        return (Point){x * cos(ang) - y * sin(ang), y * cos(ang) + x * sin(ang)};
    }
    double dis(const Point b)
    {
        return sqrt((x - b.x) * (x - b.x) + (y - b.y) * (y - b.y));
    }
};
int main()
{
    int t;
    scanf("%d", &t);
    while(t--)
    {
        vector<Point> out(4), in(4);
        for (int i = 0; i < 4;i++)
            scanf("%lf%lf", &out[i].x, &out[i].y);
        reverse(out.begin(), out.end());
        for (int i = 0; i < 4;i++)
            scanf("%lf%lf", &in[i].x, &in[i].y);
        reverse(in.begin(), in.end());
        double outang = atan2(out[1].y - out[0].y, out[1].x - out[0].x);
        vector<Point> newout(4), newin(4);
        double k = in[1].dis(in[0]) / out[1].dis(out[0]);
        for (int i = 0; i < 4;i++)
        {
            newout[i] = (out[i] - out[0]).rotate(-outang);
            newin[i] = (in[i] - out[0]).rotate(-outang);
        }
        double nowang = atan2(newin[1].y - newin[0].y, newin[1].x - newin[0].x);
        double coef = det(k * cos(nowang) - 1, -k * sin(nowang), k * sin(nowang), k * cos(nowang) - 1);
        double x = det(-newin[0].x, -k * sin(nowang), -newin[0].y, k * cos(nowang) - 1) / coef;
        double y = det(k * cos(nowang) - 1, -newin[0].x, k * sin(nowang), -newin[0].y) / coef;
        Point ans = (Point){x, y};
        ans = ans.rotate(outang) + out[0];
        printf("%.10lf %.10lf\n", ans.x, ans.y);
    }
    return 0;
}

J Planar graph

题意:给定一个平面图(有自环重边等),问为了让图中的边围成的区域均可与外界无穷区域连通所需删除的最少边数,要求在删除边数相同的时候删除边序号字典序最小。

解法:本题题意即是不能成环。使用并查集依次倒序插入边,若成环则删除即可。

#include 
using namespace std;
class DSU
{
    vector<int> father;
    int n;
    int getfather(int x)
    {
        return father[x] == x ? x : father[x] = getfather(father[x]);
    }

public:
    DSU(int n)
    {
        father.resize(n + 1);
        for (int i = 1; i <= n;i++)
            father[i] = i;
    }
    bool check(int u, int v)
    {
        return getfather(u) == getfather(v);
    }
    void merge(int u, int v)
    {
        u = getfather(u);
        v = getfather(v);
        father[u] = getfather(v);
    }
};
int main()
{
    int t, n, m;
    scanf("%d", &t);
    while(t--)
    {
        scanf("%d%d", &n, &m);
        DSU t(n);
        vector<pair<int, int>> edge(m + 1);
        for (int i = 1; i <= m;i++)
            scanf("%d%d", &edge[i].first, &edge[i].second);
        vector<int> ans;
        for (int i = m; i >= 1;i--)
        {
            if(t.check(edge[i].first, edge[i].second))
                ans.push_back(i);
            else
                t.merge(edge[i].first, edge[i].second);
        }
        reverse(ans.begin(), ans.end());
        printf("%d\n", ans.size());
        for(auto i:ans)
            printf("%d ", i);
        printf("\n");
    }
    return 0;
}

K Find different

题意:给定大小为 n n n 的环,环上每个点有一个 [ 0 , m − 1 ] [0,m-1] [0,m1] 的数值,不计旋转同构和数值循环同构(即若两个环上的数字能通过整体模 m m m 加法变成一样则认为相同),问合法环数目。 n , m ≤ 1 × 1 0 5 n,m \leq 1\times 10^5 n,m1×105

解法:本题中同时出现了两类同构:数值上 [ 0 , m − 1 ] [0,m-1] [0,m1] 的循环同构(旋转同构),和位置上 [ 1 , n ] [1,n] [1,n] 的旋转同构。可以对这 n m nm nm 个置换同时找不动点,但是过于复杂。

对于这类区间整体加同构,可以考虑使用差分数组来刻画。令 b i = a i − a ( i + n − 1 )   m o d   n b_i=a_i-a_{(i+n-1) \bmod n} bi=aia(i+n1)modn,则区间整体加对 { b } \{b\} {b} 数组无影响,此数组唯一需要满足的条件即是 ∑ b i = 0 \sum b_i=0 bi=0。因而对于长度为 k k k 的数组,其满足的 { b } \{b\} {b} 序列个数为 m k − 1 m^{k-1} mk1

考虑位置旋转同构的影响。类比 Polya 定理,对于长度为 n n n 的循环同构,枚举长度为 d , d ∣ n d,d|n d,dn 的段,使得整个环是由 n d \dfrac{n}{d} dn完全相同的长度为 d d d 的段(循环节)拼接起来的。设这一段长度为 d d d 的差分数组的和为 s s s,则利用上面的 ∑ b i = 0 \sum b_i=0 bi=0 可得 m ∣ s n d m|s\dfrac{n}{d} msdn,即 s = k m gcd ⁡ ( m , n d ) , k ∈ [ 0 , gcd ⁡ ( m , n d ) − 1 ] s=\dfrac{km}{\gcd(m,\frac{n}{d})},k \in \left[0,\gcd\left(m,\dfrac{n}{d}\right)-1\right] s=gcd(m,dn)km,k[0,gcd(m,dn)1]。因而对于整个环上,由完全相同的长度为 d d d n d \dfrac{n}{d} dn 个循环节(允许更短的循环节)构成的长度为 n n n 的差分序列()个数为 g n , d = m d − 1 gcd ⁡ ( m , n d ) g_{n,d}=m^{d-1}\gcd\left(m,\dfrac{n}{d}\right) gn,d=md1gcd(m,dn)。再容斥掉 d d d 的因子,可得整个环完全由长度为 d d d 的、不含更短循环节构成的长度为 n n n 的差分序列()个数为 f n , d = g n , d − ∑ d ′ ∣ d , d ′ ≠ d f n , d ′ \displaystyle f_{n,d}=g_{n,d}-\sum_{d'|d,d' \neq d} f_{n,d'} fn,d=gn,ddd,d=dfn,d。对于旋转同构,若其循环节为 d d d,则等价类大小为 d d d——即从 1 , 2 , ⋯   , d 1,2,\cdots,d 1,2,,d 处开始循环节的方案完全相同,因而答案需要除以 d d d,可得对于长度为 n n n答案为 ∑ d ∣ n f n , d d \displaystyle \sum_{d|n} \dfrac{f_{n,d}}{d} dndfn,d。这样直接对于每个 n n n 进行计算的时间复杂度为 O ( n log ⁡ 2 n ) \mathcal O(n \log^2 n) O(nlog2n)

一些简要说明:对于差分序列 1 − 2 − 3 − 1 − 2 − 3 1-2-3-1-2-3 123123,它计入 g 6 , 6 g_{6,6} g6,6 中,但只计入 f 6 , 3 f_{6,3} f6,3 而不计入 f 6 , 6 f_{6,6} f6,6 中(存在比 6 6 6 短的循环节长度 3 3 3)。同时该差分序列的旋转同构 2 − 3 − 1 − 2 − 3 − 1 2-3-1-2-3-1 231231 f 6 , 3 f_{6,3} f6,3 g 6 , 6 g_{6,6} g6,6 中都算做两个方案。

考虑继续化简式子。由 g n , d = ∑ d ′ ∣ d f n , d ′ \displaystyle g_{n,d}=\sum_{d'|d} f_{n,d'} gn,d=ddfn,d 利用莫比乌斯反演可得 f n , d = ∑ d ′ ∣ d μ ( d ′ ) g n , d d ′ \displaystyle f_{n,d}=\sum_{d'|d} \mu(d')g_{n,\frac{d}{d'}} fn,d=ddμ(d)gn,dd。因而总方案数 F n = ∑ d ∣ n f n , d d \displaystyle F_n=\sum_{d|n}\dfrac{f_{n,d}}{d} Fn=dndfn,d 有:
F n = ∑ d ∣ n f n , d d = ∑ d ∣ n 1 d ∑ d ′ ∣ d μ ( d d ′ ) m d ′ − 1 gcd ⁡ ( m , n d ′ ) = ∑ d ′ ∣ n gcd ⁡ ( m , n d ′ ) m d ′ − 1 ∑ k ∣ n d ′ μ ( k ) k d ′ = ∑ d ′ ∣ n 1 d ′ gcd ⁡ ( m , n d ′ ) m d ′ − 1 ( ∑ k ∣ n d ′ μ ( k ) k ) = ∑ d ′ ∣ n 1 d ′ gcd ⁡ ( m , n d ′ ) m d ′ − 1 φ ( n d ′ ) d ′ n = 1 n ∑ d ′ ∣ n gcd ⁡ ( m , n d ′ ) m d ′ − 1 φ ( n d ′ ) \begin{aligned} F_n=&\sum_{d|n}\dfrac{f_{n,d}}{d}\\ =&\sum_{d|n}\dfrac{1}{d}\sum_{d'|d}\mu\left(\dfrac{d}{d'}\right)m^{d'-1}\gcd\left(m,\dfrac{n}{d'}\right)\\ =&\sum_{d'|n}\gcd\left(m,\dfrac{n}{d'}\right)m^{d'-1}\sum_{k|\frac{n}{d'}}\dfrac{\mu(k)}{kd'}\\ =&\sum_{d'|n}\dfrac{1}{d'}\gcd\left(m,\dfrac{n}{d'}\right)m^{d'-1} \left(\sum_{k|\frac{n}{d'}}\dfrac{\mu(k)}{k}\right)\\ =&\sum_{d'|n}\dfrac{1}{d'}\gcd\left(m,\dfrac{n}{d'}\right)m^{d'-1}\varphi\left(\dfrac{n}{d'}\right)\dfrac{d'}{n}\\ =&\dfrac{1}{n}\sum_{d'|n}\gcd\left(m,\dfrac{n}{d'}\right)m^{d'-1}\varphi\left(\dfrac{n}{d'}\right) \end{aligned} Fn======dndfn,ddnd1ddμ(dd)md1gcd(m,dn)dngcd(m,dn)md1kdnkdμ(k)dnd1gcd(m,dn)md1 kdnkμ(k) dnd1gcd(m,dn)md1φ(dn)ndn1dngcd(m,dn)md1φ(dn)
因而这样的时间复杂度为 O ( n log ⁡ n ) \mathcal O(n \log n) O(nlogn)

#include 
using namespace std;
const int N = 100000;
const long long mod = 998244353;
vector<int> factor[N + 5];
bool vis[N + 5];
int prime[N + 5], tot, phi[N + 5];
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);
}
void sieve(int n)
{
    phi[1] = 1;
    for (int i = 2; i <= n;i++)
    {
        if(!vis[i])
        {
            prime[++tot] = i;
            phi[i] = i - 1;
        }
        for (int j = 1; j <= tot && (long long)prime[j] * i <= n;j++)
        {
            int num = prime[j] * i;
            vis[num] = 1;
            phi[num] = phi[i] * (prime[j] - 1);
            if (i % prime[j] == 0)
            {
                phi[num] = phi[i] * prime[j];
                break;
            }
        }
    }
    for (int i = 1; i <= n;i++)
        for (int j = i; j <= n; j += i)
            factor[j].push_back(i);
}
int main()
{
    sieve(N);
    int t, n, m;
    scanf("%d", &t);
    while(t--)
    {
        scanf("%d%d", &n, &m);
        for (int i = 1; i <= n;i++)
        {
            long long ans = 0;
            for (auto j : factor[i])
                ans = (ans + __gcd(m, i / j) * power(m, j - 1) % mod * phi[i / j] % mod) % mod;
            printf("%lld%c", ans * inv(i) % mod, " \n"[i == n]);
        }
    }
    return 0;
}

L Loop

题意:给定长度为 n n n 的序列 { a i } \{a_i\} {ai},每次可以选择一个数字放到后面,问经过 k k k 次操作后得到的字典序最大的序列。 n , k ≤ 3 × 1 0 5 n,k \leq 3\times 10^5 n,k3×105

解法:贪心的从左到右考虑,若 a i − 1 < a i a_{i-1}ai1<ai 则一定选择将 a i − 1 a_{i-1} ai1 后置,然后继续比较 a i − 2 < a i a_{i-2}ai2<ai,以此类推。因而首先执行单调栈,弹栈次数小于等于 k k k 次则选择弹栈。最后考虑这些弹出的值的插入,对弹出后的序列和弹出的值(倒序排序)进行归并排序,优先原序列的数字即可。例如 3 , 3 , 3 , 5 3,3,3,5 3,3,3,5 中插入 3 3 3 3 , 3 , 3 , 5 , 3 ′ 3,3,3,5,3' 3,3,3,5,3 3 , 3 , 3 , 2 3,3,3,2 3,3,3,2 3 , 3 , 3 , 3 ′ , 2 3,3,3,3',2 3,3,3,3,2,其中 3 ’ 3’ 3’ 为新插入数字。总时间复杂度 O ( n + n log ⁡ n ) \mathcal O(n+n \log n) O(n+nlogn)

#include 
using namespace std;
int main()
{
    int t, n, k;
    scanf("%d", &t);
    while(t--)
    {
        scanf("%d%d", &n, &k);
        vector<int> a(n + 1);
        for (int i = 1; i <= n; i++)
            scanf("%d", &a[i]);
        vector<int> out;
        vector<int> b;
        for (int i = 1; i <= n;i++)
        {
            while (!b.empty() && k && b.back() < a[i])
            {
                out.push_back(b.back());
                b.pop_back();
                k--;
            }
            b.push_back(a[i]);
        }
        sort(out.begin(), out.end(), greater<int>());
        int placea = 0, placeb = 0;
        vector<int> ans;
        while (placea < out.size() || placeb < b.size())
        {
            if (placea >= out.size() || (placeb < b.size() && b[placeb] >= out[placea]))
            {
                ans.push_back(b[placeb]);
                placeb++;
            }
            else
            {
                ans.push_back(out[placea]);
                placea++;
            }
        }
        for (int i = 0; i < n;i++)
            printf("%d%c", ans[i], " \n"[i == n - 1]);
    }
    return 0;
}

你可能感兴趣的:(补题记录,学习笔记,算法,c++,图论)