牛客多校第六场8月2日补题记录

A Contracting Convex Hull

题意:给定一个凸包,每一个时刻以 1 1 1 的速度各边在缩小, q q q 次询问,问在 t t t 时刻凸包的面积。

解法:此题困难之处在于缩小之后凸包的形状会发生变化。不难发现,每次缩小的是当前边长最小的边。当其超过边界点的时候,直接延申相邻的两条边,此时图形为这一扩展后的形状。其他的情况可以使用相似快速的计算面积。

(挖坑)

C Delete Edges

题意:给定一个由 n n n 个点组成的完全图,每次可以选择三个点 x , y , z x,y,z x,y,z,如果边 ( x , y ) , ( y , z ) , ( z , x ) (x,y),(y,z),(z,x) (x,y),(y,z),(z,x) 均存在,则删去这三条边。求一种构造可以不断的进行这样的操作使得最后剩下来的边数不超过 n n n

解法:我们需要删掉的边总数至少为 n ( n − 1 ) 2 − n \displaystyle \frac{n(n-1)}{2}-n 2n(n1)n。一开始我们可以有一个朴素的想法:找一个点出来,剩下的点两两配对,这样构成三点,这样我们删去边的次数为 n − 1 2 \displaystyle \frac{n-1}{2} 2n1 次。如果这个能重复 n n n 次我们的目标就达到了。因而,我们可以考虑重复这个过程,即每一个点都拎出来,和剩下的点两两配对。如果能做到不重复的构造就行了。

基于这个思想,我们可以考虑利用数字关系。对于第 i i i 号点,我们为其找的配对点对满足条件 j + k = n − i j+k=n-i j+k=ni 或者 j + k = 2 n − i j+k=2n-i j+k=2ni。这样一来不容易构成重复,二来容易构造。

但是这样的三元组足够多吗?考察方程 x + y + z ≡ 0 m o d    n x+y+z \equiv 0 \mod n x+y+z0modn,解的个数为 ∑ i = 1 n ⌊ n − i 2 ⌋ = n 2 − 3 n + 2 6 , n = 3 k \displaystyle \sum_{i=1}^{n} \left \lfloor \frac{n-i}{2} \right \rfloor=\frac{n^2-3n+2}{6},n=3k i=1n2ni=6n23n+2,n=3k n 2 − 3 n + 6 2 , n ≠ 3 k \displaystyle \frac{n^2-3n+6}{2},n \neq 3k 2n23n+6,n=3k。带入发现这样的数目是绝对足够的。

#include <cstdio>
#include <algorithm>
#include <vector>
using namespace std;
struct triple
{
    int a;
    int b;
    int c;
};
vector<triple> ans;
int main()
{
    int n;
    scanf("%d", &n);
    for (int i = 1; i <= n;i++)
        for (int j = i + 1; j <= n;j++)
        {
            int k = n - i - j;
            if(k<=0)
                k += n;
            if(k<=j || k==i)
                continue;
            ans.push_back((triple){i, j, k});
        }
    printf("%d\n", ans.size());
    for(auto i:ans)
        printf("%d %d %d\n", i.a, i.b, i.c);
    return 0;
}

D Gambling Monster

题意:有一个随机数发生器,以 p i p_i pi 的概率生成一个数 i i i 0 ≤ i ≤ n − 1 0 \leq i \leq n-1 0in1。初始的数为 0 0 0,记上一轮的数为 x x x,若本轮产生的随机数 y y y 满足 x ⊕ y > x x \oplus y>x xy>x,则 x x x 赋值为 x ⊕ y x \oplus y xy,否则不动。问变成 n − 1 n-1 n1 的期望轮数,满足 n = 2 k n=2^k n=2k n ≤ 2 16 n \leq 2^{16} n216

解法:首先考虑朴素 DP。记 E ( i ) E(i) E(i) 为当前数值为 i i i,变到 n − 1 n-1 n1 所需的期望步数,则 E ( n − 1 ) = 0 E(n-1)=0 E(n1)=0,待求 E ( 0 ) E(0) E(0)。考虑其转移: E ( x ) = [ E ( x ) + 1 ] [ 1 − S ( x ) ] + ∑ x < z , x ⊕ y = z ( E ( z ) + 1 ) p ( y ) \displaystyle E(x)=[E(x)+1][1-S(x)]+\sum_{x<z,x \oplus y=z} (E(z)+1) p(y) E(x)=[E(x)+1][1S(x)]+x<z,xy=z(E(z)+1)p(y),其中 S ( x ) S(x) S(x) 为当前数为 x x x,随机选取了一个数 y y y,使其变大( x ⊕ y > x x \oplus y>x xy>x)的概率。整个式子含义为,若当前的数值为 x x x,那么可能摇到了一个无法让它变大的数,那么停留在 x x x,期望步数增加 1 1 1——对应 [ E ( x ) + 1 ] [ 1 − S ( x ) ] [E(x)+1][1-S(x)] [E(x)+1][1S(x)]。否则,摇到了 y y y,轮数增加, x x x 对应变大到 x ⊕ y = z x \oplus y=z xy=z,其概率为 p ( y ) p(y) p(y)

考虑 S ( x ) S(x) S(x) 的求法。显然,只有在 x x x 对应二进制位下为 0 0 0 的位置, y y y 1 1 1,才会使 x ⊕ y x \oplus y xy 变大。考虑从高到低的枚举 x x x 的每一个二进制位,如果第 i i i 位为 0 0 0,则把最高位为第 i i i 位并且此位为 1 1 1 的值累加到 S ( x ) S(x) S(x) 中。因为如果在第 i i i 位这里变大了,后面的位其实可以不用管。

显然这样朴素转移的时间复杂度是 O ( n 2 ) O(n^2) O(n2),无法接受。考虑此处转移的一个特殊性:只会由 z > x z>x z>x 处转移到 x x x,因而可以考虑使用 cdq(陈丹琦)分治。cdq 分治原本是针对于序列上的操作,将全部修改询问操作离线化后利用时间的单调性,将操作序列按时间先后分成两个部分,先处理左侧的操作序列,再统计左侧操作对右侧的影响,再考虑右侧。此处可以类比这个操作——因为 DP 修改与赋值是单向的,只会由后侧的数对前面的数值产生影响,因而可以考虑使用 cdq 分治。

此外,注意到这个求和的条件—— x ⊕ y = z x \oplus y=z xy=z,移项得到 x = z ⊕ y x =z \oplus y x=zy。类比于多项式卷积的条件 i + j = k i+j=k i+j=k,此处是位运算的卷积,可以使用 FWT(快速沃尔什变换)在 O ( n log ⁡ n ) O(n \log n) O(nlogn) 复杂度下处理异或卷积。推荐学习本文算法笔记|快速沃尔什变换 FWT(或卷积、与卷积、异或卷积)

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

代码中有两处细节需要说明:

  1. 一是在 cdq 函数中对 FWT 数组进行预处理时。由于这里的 n = 2 k n=2^k n=2k,因而每一个二分出来的区间,其区间端点一定都是 2 2 2 的幂次的倍数,如 4 , 12 4,12 4,12 等等。其特殊性在于,其区间中点 m i d mid mid 一定是最高变化位上的一个分界。例如,在区间 [ 8 , 11 ] [8,11] [8,11] 中,其区间中点为 10 10 10 8 8 8 9 9 9 的二进制为 1000 , 1001 1000,1001 1000,1001 10 10 10 11 11 11 的二进制为 1010 , 1011 1010,1011 1010,1011。容易发现,这一中点将区间恰好分成两个部分——第二位上为 0 0 0 与为 1 1 1 的部分,而第二位是这个区间中最高变化位。因而,可以考虑将 m i d mid mid 作为转移的分界点——在 m i d mid mid 以前,其概率均为 0 0 0,认为不可以从这些数转移;比 m i d mid mid 大的则可以转移。
  2. 在 cdq 函数的边界情况的考虑。注意到我们 cdq 函数最后的操作——直接将 E ( x ) E(x) E(x) 累加上了 ∑ x < z , x ⊕ y = z ( E ( z ) + 1 ) p ( y ) \displaystyle \sum_{x<z,x \oplus y=z} (E(z)+1) p(y) x<z,xy=z(E(z)+1)p(y)。因而现在 E ( x ) E(x) E(x) 中存的不是 E ( x ) E(x) E(x) 了而是方程中的后半项,即 ∑ x < z , x ⊕ y = z ( E ( z ) + 1 ) p ( y ) \displaystyle \sum_{x<z,x \oplus y=z} (E(z)+1) p(y) x<z,xy=z(E(z)+1)p(y),因而需要通过方程的求解解出 E ( x ) E(x) E(x)。解得 E ( x ) = 1 + ∑ x < z , x ⊕ y = z ( E ( z ) + 1 ) p ( y ) S ( x ) − 1 \displaystyle E(x)=\frac{1+\sum_{x<z,x \oplus y=z} (E(z)+1) p(y)}{S(x)}-1 E(x)=S(x)1+x<z,xy=z(E(z)+1)p(y)1,这与代码中的表述相吻合。
#include <cstdio>
#include <algorithm>
#include <cmath>
using namespace std;
const long long mod = 1000000007ll;
long long power(long long a,long long x)
{
    long long ans = 1;
    while(x)
    {
        if(x&1)
            ans = a * ans % mod;
        a = a * a % mod;
        x >>= 1;
    }
    return ans;
}
long long inv(long long a)
{
    return power(a, mod - 2);
}
long long S[200005], sum[200005], E[200005], p[200005];
long long A[200005], B[200005];
void FWT(int n,long long *A,bool flag)
//flag为0表示正向变换,1表示反向变换(即反演)
{
    long long inv2 = (mod + 1) >> 1;
    for (int l = 2, m = 1; l <= n; l <<= 1, m <<= 1)
        for (int j = 0; j < n; j += l)
            for (int i = 0; i < m; i++)
            {
                long long x = A[i + j], y = A[i + j + m];
                if (!flag)
                {
                    A[i + j] = (x + y) % mod;
                    A[i + j + m] = (x - y + mod) % mod;
                }
                else
                {
                    A[i + j] = (x + y) * inv2 % mod;
                    A[i + j + m] = (x - y + mod) * inv2 % mod;
                }
            }
}

void cdq(int left,int right)
{
    if(left==right)
    {
        E[left] = ((E[left] + 1) * inv(S[left]) % mod + mod - 1) % mod;
        return;
    }
    int mid = (left + right) >> 1;
    cdq(mid + 1, right);//此处是后侧对前侧的影响。因而先处理后侧,然后是后侧对前侧的影响(见下),即是异或卷积部分
    int len = right - left + 1;
    for (int i = left; i <= mid;i++)
        A[i - left] = 0;
    for (int i = mid + 1; i <= right;i++)
        A[i - left] = E[i] + 1;
    for (int i = 0; i < len;i++)
        B[i] = p[i];
    FWT(len, A, 0);
    FWT(len, B, 0);
    for (int i = 0; i < len; i++)
        A[i] = (A[i] * B[i]) % mod;
    FWT(len, A, 1);
    for (int i = left; i <= mid;i++)
        E[i] = (E[i] + A[i - left]) % mod;
    cdq(left, mid);
    //最后处理左侧区间
}
int main()
{
    int n, t, m;
    scanf("%d", &t);
    while(t--)
    {
        scanf("%d", &n);
        m = log2(n);
        long long tot = 0;
        for (int i = 0; i < n;i++)
            E[i] = S[i] = 0;
        for (int i = 0; i < n; i++)
        {
            scanf("%lld", &p[i]);
            tot = (tot + p[i]) % mod;
        }
        tot = inv(tot);
        for (int i = 0; i < n;i++)
        {
            p[i] = p[i] * tot % mod;
            if (i)
                sum[i] = (sum[i - 1] + p[i]) % mod;
            else
                sum[i] = p[i];
        }
        for (int i = 0; i < n;i++)
            for (int j = m - 1; j >= 0;j--)
                if(!(i&(1<<j)))
                    S[i] = (S[i] + (sum[(1 << (j + 1)) - 1] - sum[(1 << j) - 1]) % mod + mod) % mod;
        cdq(0, n - 1);
        printf("%lld\n", E[0] + 1);
    }
    return 0;
}

G Hasse Diagram

题意:Hasse 图上刻画了 n n n 的全部整除关系。记 H n H_n Hn n n n 的 Hasse 图上边的数目,求 ∑ i = 1 n H i \sum_{i=1}^{n} H_i i=1nHi n ≤ 1 × 1 0 10 n \leq 1\times 10^{10} n1×1010

解法:记 f ( n ) f(n) f(n) 为以 n n n 为根节点的 Hasse Diagram 的边数量, n = ∏ i = 1 k p i a i \displaystyle n=\prod_{i=1}^k p_i ^{a_i} n=i=1kpiai,则可以得到, ∀ i ∈ [ 1 , k ] , f ( n ) = ( a i + 1 ) f ( n p i a i ) + a i d ( n p i a i ) \displaystyle \forall i \in [1,k],f(n)=(a_i+1)f\left(\frac{n}{p_{i}^{a_i}}\right)+a_id\left(\frac{n}{p_i ^{a_i}}\right) i[1,k],f(n)=(ai+1)f(piain)+aid(piain),其中 d ( x ) d(x) d(x) 表示 x x x 的因数数量。

有这一递推之后,可以考虑使用 min25 筛。Min_25 筛 一文中有普通 min25 筛的详细介绍,此处不再详细介绍 min25 筛的算法流程,此处只针对本题需要使用到的部分进行一些叙述。本文使用的代码来源:牛客多校6G - Hasse Diagram(min25),写的较为清晰。

首先考虑第一步,设计 g ( x ) g(x) g(x),并进行递推。“我们定义 g ( x ) ( x ∈ N ) g(x)(x \in \N) g(x)(xN) 表示把 x x x 当成质数计算 f ( x ) f(x) f(x) 得到的结果。”不难发现,对于质数 p p p f ( p ) = g ( p ) = 1 f(p)=g(p)=1 f(p)=g(p)=1,那么一个非常朴素的想法就是令合数 x x x g ( x ) g(x) g(x) 也取 1 1 1。那么初始的 G ( i , 0 ) = ∑ k = 2 i g ( k ) = i − 1 \displaystyle G(i,0)=\sum_{k=2}^i g(k)=i-1 G(i,0)=k=2ig(k)=i1 i ∈ { x ∣ x = ⌊ n d ⌋ , d ∈ N } \displaystyle i \in \left\{x|x={\left\lfloor \frac{n}{d} \right\rfloor},d\in \N\right\} i{xx=dn,dN}。之后的 F ( i , j ) , G ( i , j ) F(i,j),G(i,j) F(i,j),G(i,j) i i i 的定义域相同,而 j ∈ [ 0 , k ] j \in [0,k] j[0,k] k k k 为小于 n \sqrt{n} n 的质数个数。

p r i m e j {\rm prime}_j primej 为第 j j j 个质数, S ( j ) = ∑ k = 1 j g ( p r i m e k ) = j \displaystyle S(j)=\sum_{k=1}^j g({\rm prime}_k)=j S(j)=k=1jg(primek)=j。从小到大的枚举质数,第 j j j 轮除掉最小质因子为 p r i m e j {\rm prime}_j primej 的合数对答案的贡献,即 G ( i , j ) ← G ( i , j ) − G ( ⌊ i p r i m e j ⌋ , j − 1 ) − S ( j − 1 ) \displaystyle G(i,j) \leftarrow G(i,j)-G\left(\left \lfloor \frac{i}{{\rm prime}_j} \right \rfloor,j-1\right )-S(j-1) G(i,j)G(i,j)G(primeji,j1)S(j1)。其含义为,所有小于 i p r i m e j \displaystyle \frac{i}{{\rm prime}_j} primeji 的数乘以 p r i m e j { \rm prime}_j primej 后对答案的贡献要去除。

void solve(ll n) { // part 1
    int cur = 0;
    int mx = 0;
    m = 0;
    while(m * m <= n) {
        if(pri[mx] <= m) mx++;//找到最大的质数是哪个质数
        m++;
    }
    ll i = 1;
    while(i <= n) {
        num[cur] = n / i;//划分块的长度
        ans[cur] = (num[cur] - 1) % M;//这一块的答案即是n/i-1
        if(num[cur] <= m) id1[num[cur]] = cur;
        else id2[n / num[cur]] = cur;
        cur++;
        i = n / (n / i) + 1;
    }
    for(int i = 1; i < mx; i++) {
    //从小到大的枚举质数
        for(int j = 0; j < cur && 1ll * pri[i] * pri[i] <= num[j]; j++) {
            //枚举块的分段点,对每一块都算出对应的G(i,j)。注意此处压缩了空间
            ans[j] -= (ans[ID(num[j] / pri[i])] - (i - 1)) % M;
            ans[j] %= M;
        }
    }
}

经过 O ( n log ⁡ n ) \displaystyle O\left(\dfrac{\sqrt{n}}{\log \sqrt{n}}\right ) O(logn n ) 次筛选,我们得到了 G ( n , k ) G(n,k) G(n,k),即全部质数的答案。容易发现, F ( n , k ) = G ( n , k ) F(n,k)=G(n,k) F(n,k)=G(n,k),而我们所需要的答案是 F ( n , 0 ) F(n,0) F(n,0),即所有质数和合数的 f f f 和,进入第二步操作。

容易注意到我们的递推式并不是完全积性的,而是只有一部分有积性(因子个数),但是可以根据质因子进行计算。于是考虑在递推因子个数的同时计算 f f f

正常的操作方法是从大到小的枚举质数,依次加入以这些质数为最小质因子的合数的正确的 f f f 的贡献。“从大到小枚举 p r i m e j {\rm prime}_j primej,把最小质因子恰好为 p r i m e j {\rm prime}_j primej 的那些合数的 f f f 加到 F F F 中去。枚举大于等于 p r i m e j 2 {\rm prime}_j^2 primej2 的所有 i i i,再额外枚举 p r i m e j e + 1 {\rm prime}_j^{e+1} primeje+1 的指数 e e e F ( i , j − 1 ) ← F ( i , j − 1 ) + F ( ⌊ i p r i m e j e ⌋ , j ) × f ( p r i m e j e ) + f ( p r i m e j e + 1 ) \displaystyle F(i,j−1) \leftarrow F(i,j-1)+F\left(\left \lfloor \frac{i}{{\rm prime}_j ^{e}} \right \rfloor,j\right ) \times f({\rm prime}_j ^{e})+f({\rm prime}_j ^{e+1}) F(i,j1)F(i,j1)+F(primejei,j)×f(primeje)+f(primeje+1)”。引用部分的 f ( p r i m e j e ) f({\rm prime}_j ^{e}) f(primeje) 为因子个数,此处答案为 e + 1 e+1 e+1 F ( i , j ) F(i,j) F(i,j) 是因子个数和。在本题中,我们还要根据因子个数继续计算出真正的 f f f,即 Hasse Diagram 的边数量。

具体做法可以详见下面的代码片段。

PII F(ll n, int k) { // part 2
    ll res = 0; // f(x)
    ll d = 0; // 约数个数

    for(int i = k; 1ll * pri[i] * pri[i] <= n; i++) {
        ll p = pri[i];
        int cnt = 1;//即指数e
        while(p * pri[i] <= n) {
            auto par = F(n / p, i + 1);//first 为 F,second 为我们需要的答案
            d += ((cnt + 1) * par.first % M + (cnt + 2)) % M;//因子个数和的统计,乘以 f=e+1,补上 e+2
            res += ((cnt + 1) * par.second % M + cnt * par.first % M + (cnt + 1)) % M;
            //答案即是根据递推式而来:小于i/prime[j] 的数乘以prime[j] 得到当前的数,前面有系数 e+1,再加上e乘以因数个数和,再补上 prime[j] 的e次方的答案e+1。 
            res %= M;
            d %= M;
            p = p * pri[i];
            cnt++;
        }
    }
    res += ans[ID(n)] - ((k - 1) ? ans[ID(pri[k - 1])] : 0) % M;//补上这一块质数的贡献
    d += 2 * (ans[ID(n)] - ((k - 1) ? ans[ID(pri[k - 1])] : 0)) % M;//质数的因子个数和即是 Hasse Diagram 图边数目的两倍
    return {d % M, res % M};
}

最后答案即是 F(n,1) 的第二个元素了。

H Hopping Rabbit

题意:在 x O y xOy xOy 平面上有 n n n 个矩形,问是否存在一点 ( x , y ) (x,y) (x,y),使得从这一点任意的向 x x x 轴正、负向, y y y 轴正、负向跳 d d d 步(允许多次)都不会落入任何一个矩形中。 n , d ≤ 1 × 1 0 5 n, d\leq 1\times 10^5 n,d1×105

解法:从一个点 ( x , y ) (x,y) (x,y) 跳出去可以到达的点为 ( x + k 1 d , y + k 2 d ) , k 1 , k 2 ∈ Z (x+k_1d,y+k_2d),k_1,k_2 \in \Z (x+k1d,y+k2d),k1,k2Z。因而不妨取模,将所有的点与矩形全部集中在 S = [ 0 , d − 1 ] × [ 0 , d − 1 ] S=[0,d-1] \times [0,d-1] S=[0,d1]×[0,d1] 中。

考虑如何将矩形放入 S S S 中。如果其长大于 d d d,则映射到 S S S 中则是一个横向覆盖了 [ 0 , d − 1 ] [0,d-1] [0,d1] 的长条;如果宽大于 d d d 则同理,是一个纵向覆盖了 [ 0 , d − 1 ] [0,d-1] [0,d1] 的竖条。

如果长或者宽不超过 d d d,则一定没有完全覆盖 [ 0 , d − 1 ] [0,d-1] [0,d1],宽同理。下面以长举例:考虑对左右边界 x 1 , x 2 x_1,x_2 x1,x2 取模。如果 x 1 ≤ x 2 x_1 \leq x_2 x1x2,则是分在一个 d × d d \times d d×d 的块中,映射到 S S S 就是直接的 x 1 , x 2 x_1,x2 x1,x2;否则,在原图上则是覆盖了两个 d × d d \times d d×d 的块,因而此时需要拆分成左侧的 [ 1 , x 2 ] [1,x_2] [1,x2] 与右侧的 [ x 1 , d ] [x_1,d] [x1,d] y y y 方向上同理。

覆盖完成之后,只需要像扫描线那样,一排排的处理,不断的覆盖、删去矩形即可。可以维护一个区间最小值,如果最小值为 0 0 0,则证明这一排有未覆盖的部分,可以直接枚举找到即可。当然也可以求出所有矩形在 S S S 上的覆盖面积。二者都可以使用扫描线快速的求出。

注意到此题 d d d 不是很大,因而不需要离散化处理。整体复杂度 O ( n log ⁡ n ) O(n \log n) O(nlogn)

#include <cstdio>
#include <algorithm>
#include <vector>
using namespace std;
const int inf = 0x3f3f3f3f;
struct node
{
    int minimum;
    int tag;
};
struct node t[400005];
void change(int place, int left, int right, int start, int end,int x);
void pushdown(int place,int left,int right)
{
    if(t[place].tag)
    {
        int mid = (left + right) >> 1;
        change(place << 1, left, mid, left, mid, t[place].tag);
        change(place << 1 | 1, mid + 1, right, mid + 1, right, t[place].tag);
        t[place].tag = 0;
    }
    return;
}
void change(int place,int left,int right,int start,int end,int x)
{
    if(start<=left && right<=end)
    {
        t[place].minimum += x;
        t[place].tag += x;
        return;
    }
    pushdown(place, left, right);
    int mid = (left + right) >> 1;
    if(start<=mid)
        change(place << 1, left, mid, start, end, x);
    if(end>mid)
        change(place << 1 | 1, mid + 1, right, start, end, x);
    t[place].minimum = min(t[place << 1].minimum, t[place << 1 | 1].minimum);
}
int query(int place,int left,int right,int start,int end)
{
    if(start<=left && right<=end)
        return t[place].minimum;
    pushdown(place, left, right);
    int mid = (left + right) >> 1;
    int ans = inf;
    if(start<=mid)
        ans = min(ans, query(place << 1, left, mid, start, end));
    if(end>mid)
        ans = min(ans, query(place << 1 | 1, mid + 1, right, start, end));
    return ans;
}
struct line
{
    int left;
    int right;
    int flag;
};
vector<line> op[100005];
int main()
{
    int n, d;
    int x1, y1, x2, y2;
    scanf("%d%d", &n, &d);
    for (int i = 1; i <= n;i++)
    {
        scanf("%d%d%d%d", &x1, &y1, &x2, &y2);
        //由于它是跳在格子正中,因而真正在覆盖的时候x2与y2都需要减一
        int left, right, up, down;
        if (x2 - x1 >= d)//先判定长度是否大于 d,不要贸然取模
        {
            left = 1;
            right = d;
        }
        else
        {
            left = (x1 % d + d) % d + 1;
            right = ((x2 - 1) % d + d) % d + 1;
        }
        if (y2 - y1 >= d)
        {
            down = 1;
            up = d;
        }
        else
        {
            down = (y1 % d + d) % d + 1;
            up = ((y2 - 1) % d + d) % d + 1;
        }
        if (left<=right)//分横向是否被分成两块
        {
            if (down<=up)//分纵向是否被分成两块
            {
                op[down].push_back((line){left, right, 1});
                op[up + 1].push_back((line){left, right, -1});
            }
            else
            {
                op[1].push_back((line){left, right, 1});
                op[up + 1].push_back((line){left, right, -1});
                op[down].push_back((line){left, right, 1});
                op[d + 1].push_back((line){left, right, -1});
            }
        }
        else
        {
            if (down<=up)
            {
                op[down].push_back((line){1, right, 1});
                op[down].push_back((line){left, d, 1});
                op[up + 1].push_back((line){1, right, -1});
                op[up + 1].push_back((line){left, d, -1});
            }
            else
            {
                op[1].push_back((line){1, right, 1});
                op[1].push_back((line){left, d, 1});
                op[up + 1].push_back((line){1, right, -1});
                //纵向上采用了差分的思想:只在要改变的时候再修改。因而上边界都要加1
                op[up + 1].push_back((line){left, d, -1});
                op[down].push_back((line){1, right, 1});
                op[down].push_back((line){left, d, 1});
                op[d + 1].push_back((line){1, right, -1});
                op[d + 1].push_back((line){left, d, -1});
            }
        }
    }
    for (int i = 1; i <= d;i++)
    {
        for (auto j : op[i])//要将一行处理完再下定论
            change(1, 1, d, j.left, j.right, j.flag);
        if (query(1, 1, d, 1, d) == 0)
        {
            for (int j = 1; j <= d;j++)
                if (query(1, 1, d, j, j) == 0)
                {
                    printf("YES\n%d %d\n", j - 1 + d, i - 1 + d);
                    break;
                }
            return 0;
        }
    }
    printf("NO");
    return 0;
}

J Defend Your Country

题意:给定一张无向连通图,每个点有点权 a i a_i ai。现在可以从原图上删除一些边,将图分成若干个连通块。每一个连通块 S S S 的总权值等于 ( − 1 ) n ∑ i ∈ S a i \displaystyle (-1)^n\sum_{i \in S} a_i (1)niSai,其中 n n n 为连通块 S S S 的点数。问所有连通块总的权值和最大为多少。 n ≤ 1 × 1 0 6 n \leq 1\times 10^6 n1×106

解法:显然,如果全部点数(即原式连通块大小)为偶数,那么根本不用切分。因而接下来的情况均是考虑奇数个点的连通块情况。

考虑一个奇数个点的连通块。如果当前这个连通块内点的最小权值 a i a_i ai 对应的点 i i i 不是这个连通块的割点,即去掉后原连通块不会变成若干个子连通块,那么显然切除它是最优选择;或者,去掉它之后,整个连通块变成了它和若干个偶数大小的子连通块,那么也是切除它。

并且,最后不会留有一个奇数大小的连通块——因为切到最后,最坏不过全切成单点,这样都不会变坏;而且切到只剩三个点的时候,一定有一个非割点的存在,那么至少可以保全一个偶数大小(为 2 2 2)的连通块,这样答案更优。

那么现在的情况就是最小权值刚好为割点的情况。显然,不会切除偶数个割点——否则整个初始连通块大小为奇数,切完之后大于 1 1 1 的连通块点数之和还为奇数,矛盾。因而现在切有奇数个割点。

考虑这些割点的排布。如果将原图放在圆方树上,割点也放入,则它们构成了一棵树。如果一个图上割去了大于 1 1 1 个割点,则必然存在一个割点,它与多个节点相邻,而割点都应该是叶子节点,矛盾。因而只割去一个割点

因此只需要枚举一个点,如果是割点,则看它的权值是否满足全部的子连通块均为偶数;反之直接统计权值。对于那些切分下来存在奇数大小连通块的,不予考虑。

整体复杂度 O ( n ) O(n) O(n)

#include <cstdio>
#include <algorithm>
using namespace std;
const int N = 1000000;
const long long inf = 0x3f3f3f3f3f3f3f3fll;
struct line
{
    int from;
    int to;
    int next;
};
struct line que[2 * N + 5];
int cnt, headers[N + 5];
int dfn[N + 5], low[N + 5], ind, siz[N + 5];
bool cut[N + 5];
void add(int from,int to)
{
    cnt++;
    que[cnt].from = from;
    que[cnt].to = to;
    que[cnt].next = headers[from];
    headers[from] = cnt;
}
long long minimum = inf;
long long a[N + 5];
void tarjan(int u,int father)
{
    dfn[u] = low[u] = ++ind;
    siz[u] = 1;
    int child = 0;
    bool ok = 1;
    for (int i = headers[u]; i; i = que[i].next)
        if(!dfn[que[i].to])
        {
            tarjan(que[i].to, father);
            low[u] = min(low[u], low[que[i].to]);
            siz[u] += siz[que[i].to];
            if(low[que[i].to]>=dfn[u])
            {
                if(u!=father)
                    cut[u] = 1;
                else
                    child++;
                if(siz[que[i].to]%2==1)
                    ok = 0;
            }
        }
        else
            low[u] = min(low[u], dfn[que[i].to]);
    if(child>=2)
        cut[u] = 1;
    if(!cut[u])
        minimum = min(minimum, a[u]);
    if(cut[u] && ok)
        minimum = min(minimum, a[u]);
}
int main()
{
    int n, m, t, u, v;
    scanf("%d", &t);
    while(t--)
    {
        scanf("%d%d", &n, &m);
        cnt = ind = 0;
        for (int i = 1; i <= n;i++)
            headers[i] = dfn[i] = low[i] = siz[i] = cut[i] = 0;
        minimum = inf;
        long long sum = 0;
        for (int i = 1; i <= n;i++)
        {
            scanf("%lld", &a[i]);
            sum += a[i];
        }
        for (int i = 1; i <= m; i++)
        {
            scanf("%d%d", &u, &v);
            add(u, v);
            add(v, u);
        }
        if(n%2==0)
        {
            printf("%lld\n", sum);
            continue;
        }
        tarjan(1, 1);
        printf("%lld\n", sum - 2*minimum);
    }
    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值