2024东北四省赛题解( B C 待补充 )

Codeforces gym 链接 : https://codeforces.com/gym/105173

本来还打算补一下 B \rm B B 题的,摆了,短期内可能不会补了。

C \rm C C 题补题的可能性就更小力。

一些总结与技巧

  • std::popcount() 函数,返回输入数据中,二进制中 1 的个数。这个函数只支持无符号整型,因此对有符号数据类型需要进行强制类型转换。
  • 计算几何的直线,用参数方程有时可以更简便。
  • 组合数学有时换换思路会更好做,某两个等价的情形用公式形式可能极难证明等价且极难进行变换。
  • F 题的答案数量级估计 :待补充

J. Breakfast

超级签到题,但是没有注意小数位数的问题,吃了一发罚时

D. nIM gAME

博弈。 n n n 个石子,每次可以拿走 1 1 1 个或 2 2 2 个或 3 3 3 个,先手每次操作的异或和为 0 0 0 则先手胜利。

发现 1 ≤ n ≤ 6 1 \le n \le 6 1n6 时先手必败,猜测先手必败。发现轮到后手拿时,若 3 ≤ n ≤ 6 3 \le n \le 6 3n6 且先手异或和为 0 ∼ 3 0 \sim 3 03 ,则先手必败。初始 n > 6 n > 6 n>6 时也总是能导向这个局面,因此先手必败。

A. Paper Watering

给定两个数 x x x ( 1 ≤ x ≤ 1 0 9 ) (1 \le x \le 10^9) (1x109) , k k k ( 0 ≤ x ≤ 1 0 9 ) (0 \le x \le 10^9) (0x109) ,有两种操作 :

  • x = ⌊ x ⌋ x = \lfloor{\sqrt x}\rfloor x=x
  • x = x 2 x = x^2 x=x2

最多进行 k k k 次操作,能产生多少不同的数?

x = 1 x = 1 x=1 时直接特判答案为 1 1 1

x > 1 x > 1 x>1 时将 x x x 一直进行操作 1 1 1 直到用完操作次数或者 x = 1 x = 1 x=1 , 得到序列 { a 0 = x , a 1 , a 2 , a 3 , …   a k } \{a_0 = x , a_1, a_2, a_3, \dots\, a_k \} {a0=x,a1,a2,a3,ak} { a 0 = x , a 1 , a 2 , a 3 , …   1 } \{a_0 = x , a_1, a_2, a_3, \dots\, 1 \} {a0=x,a1,a2,a3,1}

答案一开始设置为 k + 1 k + 1 k+1 ,即 { x 1 , x 2 , x 4 , … , x 2 k } \{x^1, x^2, x^4, \dots, x ^ {2^{k}}\} {x1,x2,x4,,x2k}

i = 1 i = 1 i=1 开始遍历,如果 a i 2 = a i − 1 a_i^2 = a_{i-1} ai2=ai1 ,答案加 1 1 1 , 即加上 a i a_i ai 这个可能的结果;再如果 a i = 1 a_i=1 ai=1 则答案加 1 1 1 ;否则答案加 k + 1 − i k + 1 - i k+1i , 即加上 { a i , a i 2 , a i 4 , … , a i 2 k − i } \{a_i, a_i^2, a_i^4, \dots, a_i^{2^{k - i}}\} {ai,ai2,ai4,,ai2ki}

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;

int main() {
    int x, k; cin >> x >> k;
    vector<int> v;
    if(x == 1) {cout << "1\n"; return 0;}
    for(int i = 0; i <= k; i++) {
        v.push_back(x);
        if(x == 1) break;
        x = (int)sqrt(x);
    }
    ll ans = k + 1;
    for(int i = 1; i < v.size(); i++) {
        if(v[i] * v[i] == v[i - 1]) ans++;
        else if(v[i] == 1) ans++;
        else ans += k + 1 - i;
    }
    cout << ans << endl;
    return 0;
}

E. Checksum

这题出锅了,中文写的题面出现了歧义,最后两种理解都算对。我和出题人理解的不一样,第一发 WA ,回去改bug ,改了半天,换了个方法写了对拍,40分钟后仍然没有找出bug ,打算认命再交一发,这时才发现公告。过了几分钟,第一发重测过了。

给你一个 n n n 位二进制数 A A A ,需要构造一个最小的长度为 k k k 的二进制数 B B B,使得 B ≡ p o p c o u n t ( A ) + p o p c o u n t ( B )    ( m o d 2 k ) B \equiv popcount(A) + popcount(B) \,\, \pmod {2^k} Bpopcount(A)+popcount(B)(mod2k) , 1 ≤ n ≤ 1 0 5 1 \le n \le 10^5 1n105 , 1 ≤ k ≤ 20 1 \le k \le 20 1k20

赛场上的写法有些繁琐了,大概思路是对于每一个 k k k , 枚举所有的答案 B = 0 ∼ 2 k − 1 B = 0 \sim 2^k - 1 B=02k1 ,那么 B B B 就可以作为 p o p c o u n t ( A ) = B − p o p c o u n t ( B ) popcount(A) = B - popcount(B) popcount(A)=Bpopcount(B) A A A 的答案了

题解写法是枚举 B B B 中的 1 1 1 的数量然后直接判断合法性,好懂好写,下面的代码就是这么写的。

std::popcount() 函数的名字赛场上没有想起来,自己写了一个,肯定没有它快。这个函数只支持无符号整型,因此对 int 需要进行强制类型转换。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;

int main() {
    ios::sync_with_stdio(0);
    cin.tie(0), cout.tie(0);
    int T; cin >> T;
    while(T--) {
        int n, k, x = 0, ans = 1e9; 
        cin >> n >> k;
        string s; cin >> s;
        for(char ch : s) x += (ch ^ 48);
        for(int i = 0; i <= k; i++) {
            if(popcount((unsigned)((x + i) % (1 << k))) == i)
                ans = min(ans, (x + i) % (1 << k));
        }
        if(ans > (int)1e8) cout << "None\n";
        else {
            for(int i = (1 << (k - 1)); i > 0; i >>= 1) {
                if(ans & i) cout << '1'; else cout << '0';
            }
            cout << '\n';
        }
    }
    return 0;
}

L. Bracket Generation

这题卡了两个小时,排名靠前的队伍里面只有我们一直没做出来,我们一直在想 O ( n 2 ) O(n^2) O(n2) 的DP做法怎么想办法优化到可接受的复杂度,最后发现换个想法就是 O ( n ) O(n) O(n) 的,给我难受坏了。

一个序列 S S S 初始为空,每次可以选择以下两种操作中的一种:

  • 在序列最右端添加一对小括号,即 S → S ( ) S \rightarrow S() SS()
  • 选择 S S S 的一个区间 [ l , r ] [l,r] [l,r] ,要求区间内是一个合法的括号序列,然后在这个区间外增加一对括号将其括在其中。

给定一个经过上述操作生成出的括号序列 A A A ,请问有多少种不同的生成方式可以生成 A A A ( 1 ≤ ∣ A ∣ ≤ 1 0 6 ) (1 \le |A| \le 10^6) (1A106)

由于 A A A 是生成出来的,所以一定是合法的。对合法的序列,我们将括号进行匹配,若左右括号挨在一起称这对括号为小括号,否则称大括号。显然:

  • 小括号必然来自操作 1 1 1
  • 大括号必然来自操作 2 2 2
  • 操作序列中, 1 1 1 号操作有序,即必须已经添加了第 i i i 对小括号才能添加第 i + 1 i + 1 i+1 对小括号
  • 操作序列中, 2 2 2 号操作无序,它们相互之间没有任何关系
  • 对一对大括号来说,设其中最后一个小括号为第 k k k 个,这个大括号的添加必须位于第 k k k 次操作 1 1 1 之后。

写下一个只有操作 1 1 1 的操作序列,然后把操作 2 2 2 从后往前一个一个插入进去。以样例 2 2 2 为例 :

(   (   (   )   (   )   )   (   )   (   )   )   (   (   )   ) (\,(\,(\,)\,(\,)\,)\,(\,)\,(\,)\,)\,(\,(\,)\,) ((()())()())(()) 5 5 5 对小括号,全部是操作 1 1 1

(   (   1   1   )   1   1   )   (   1   ) (\,(\,1\,1\,)\,1\,1\,)\,(\,1\,) ((11)11)(1) 有三对大括号,对应的最后一个小括号 k k k 分别为 2 , 4 , 5 2,4,5 2,4,5

写下操作序列 1 , 1 , 1 , 1 , 1 1,1,1,1,1 1,1,1,1,1 k = 5 k = 5 k=5 的操作 2 2 2 只有一个位置可以插入; k = 4 k = 4 k=4 的操作 2 2 2 本来有 2 2 2 个位置,但由于后面插入了一次操作,现在有 3 3 3 个位置可以插入;同理, k = 2 k = 2 k=2 的操作 2 2 2 本来有 4 4 4 个位置,现在有 6 6 6 个位置。答案就是 1 × 3 × 6 = 18 1 \times 3 \times 6 = 18 1×3×6=18

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int mod = 998244353;

int main() {
    ios::sync_with_stdio(0);
    cin.tie(0), cout.tie(0);
    string s; cin >> s; int k = 0;
    vector<int> v;
    for(int i = 0; i < s.length(); i++) {
        if(s[i] == ')') {
            if(s[i - 1] == '(') k++;
            else v.push_back(k);
        }
    }
    ll ans = 1;
    for(int i = v.size() - 1, j = 0; i >= 0; i--) {
        ans = ans * (k - v[i] + 1 + j) % mod; j++;
    }
    cout << ans << endl;
    return 0;
}

M. House

计算几何,题面很简单

M_House.png

思路是枚举所有的三元组 ( u , v , w ) (u,v,w) (u,v,w) 作为房子的 A , D , E A,D,E A,D,E 点 , 由这三点算出 B , C B,C B,C 点,然后在点集中寻找是否有这两个点。

这题我的写法也不是很优雅。我的写法:

A D → \overrightarrow{AD} AD 可以表示为参数形式,即 D = A + t u → D = A + t \overrightarrow{u} D=A+tu ,其中 u → = ( Δ x , Δ y ) \overrightarrow{u} = (\Delta x, \Delta y) u =(Δx,Δy) 是最小的整向量。令 v → = ( Δ y , − Δ x ) \overrightarrow{v} = (\Delta y, - \Delta x) v =(Δy,Δx) ,也就是 u → \overrightarrow{u} u 顺时针旋转 90 ° 90 \degree 90° 后得到的方向向量。于是 C C C 可以表示为 D + t v → D + t \overrightarrow{v} D+tv 。由等腰三角形的等腰关系列出方程
( X E − X D ) 2 + ( Y E − Y D ) 2 = ( X E − X D − t Δ y ) 2 + ( Y E − Y D + t Δ x ) 2 (X_E - X_D)^2 + (Y_E - Y_D)^2 = (X_E - X_D - t \Delta y)^2 + (Y_E - Y_D + t \Delta x)^2 (XEXD)2+(YEYD)2=(XEXDtΔy)2+(YEYD+tΔx)2
解得
t = 0 或 t = 2 Δ y ( X E − X D ) − Δ x ( Y E − Y D ) ( Δ x ) 2 + ( Δ y ) 2 = 2 v → ⋅ E D → ∣ ∣ v → ∣ ∣ 2 t = 0 \quad 或 \quad t = 2 \frac{\Delta y(X_E - X_D) - \Delta x(Y_E - Y_D)}{(\Delta x)^2 + (\Delta y)^2} = 2 \frac{\overrightarrow{v} \cdot \overrightarrow{ED}}{||\overrightarrow{v}||^2} t=0t=2(Δx)2+(Δy)2Δy(XEXD)Δx(YEYD)=2∣∣v 2v ED

t t t 取后者,则 C = D + t v → ,   B = A + t v → C = D + t \overrightarrow{v} , \, B = A + t \overrightarrow{v} C=D+tv ,B=A+tv ,当且仅当 t t t 是整数时, B , C B,C B,C 是整点。由于两个向量的点乘会达到 ± 8 × 1 0 18 \pm 8 \times 10^{18} ±8×1018 ,采用 __int128_t 防止溢出。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int e9 = 1e9;
#define h(x,y) (x*e9+y)
ll x[305], y[305];

int main() {
    ios::sync_with_stdio(0);
    cin.tie(0), cout.tie(0);
    int n, ans = 0; cin >> n;
    unordered_set<ll> se;
    for(int i = 1; i <= n; i++) {
      cin >> x[i] >> y[i];
       se.insert(h(x[i], y[i]));
    }
    for(int a = 1; a <= n; a++) {
        for(int d = 1; d <= n; d++) {
            for(int e = 1; e <= n; e++) {
                if(a == d || a == e || d == e) continue;
                if((x[a] - x[d]) * (x[e] - x[d]) >= (y[d] - y[a]) * (y[e] - y[d])) continue;
                if((x[a] - x[d]) * (y[e] - y[d]) <= (y[a] - y[d]) * (x[e] - x[d])) continue;

                ll dx = x[d] - x[a], dy = y[d] - y[a];
                if(dx == 0) dy = 1; else if(dy == 0) dx = 1;
                else {ll g = __gcd(dx, dy); dx /= g; dy /= g;}

                __int128_t t1 = 2 * (__int128_t(dy) * (x[e] - x[d]) - dx * (y[e] - y[d]));
                __int128_t t2 = __int128_t(dx) * dx + dy * dy;
                if(t1 % t2 != 0) continue;

                ll t = t1 / t2;
                ll xc = x[d] + t * dy, yc = y[d] - t * dx, xb = x[a] + t * dy, yb = y[a] - t * dx;
                if(se.count(h(xc, yc)) && se.count(h(xb, yb))) ans++;
            }
        }
    }
    cout << ans << endl;
    return 0;
}

F. Factor

给定 p , x , k p, x, k p,x,k ( 1 ≤ p , x ≤ 1 0 14 , 2 ≤ k ≤ 1 0 14 ) (1 \le p, x \le 10^{14}, 2 \le k \le 10^{14}) (1p,x1014,2k1014) ,求 表示在 1 1 1 x x x 的范围内,使得 p q \frac{p}{q} qp k k k 进制下是一个有限小数的整数 q q q 的数量。

p q \frac{p}{q} qp k k k 进制下是有限小数,则 p k ∞ ≡ 0 ( m o d q ) pk^{\infty} \equiv 0 \pmod{q} pk0(modq) 。也就是说,令 q = p 1 a 1 p 2 a 2 p 3 a 3 … q = p_1^{a_1} p_2^{a_2} p_3^{a_3} \dots q=p1a1p2a2p3a3 , 对任意 p i p_i pi 都有 p i ∣ k p_i | k pik p i ∣ p p_i | p pip a i a_i ai 小于等于 p p p p i p_i pi 的幂次,因此列举出所有质因数再 DFS 即可。

赛场上想出了这题的正解,但是没有写,因为没有想到答案最多为 1 0 8 10^8 108 左右,以为 DFS 复杂度超标。另一个原因是我们觉得 L L L 题更有希望(毕竟同排名只有我们没写出来),放弃了思考 F F F 题。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
ll p, x, k, ans, pri[50];
int num[50], cnt;

void DFS(ll now, int i) {
    for(int t = num[i]; ; ) {
        if(i < cnt) DFS(now, i + 1);
        if(t > 0 && now <= x / pri[i])
            ans++, t--, now *= pri[i];
        else break;
    }
}

int main() {
    cin >> p >> x >> k;
    unordered_map<ll, ll> mp;
    for(ll i = 2; i * i <= p; i++) 
        while(p % i == 0) p /= i, mp[i]++;
    if(p > 1) mp[p]++;
    for(ll i = 2; i * i <= k; i++) 
        while(k % i == 0) k /= i, mp[i] += 64;
    if(k > 1) mp[k] += 64;
    for(auto pi : mp) {
        pri[cnt] = pi.first;
        num[cnt++] = pi.second;
    }
    cnt--; DFS(1, 0);
    cout << ans + 1 << endl;
    return 0;
}

I. Password

有一个数字序列,每一项都是 1 ∼ k 1 \sim k 1k 之间的整数。 如果序列的某个区间 [ i , i + k − 1 ] [i, i + k − 1] [i,i+k1] 1 ∼ k 1 \sim k 1k 的一个排列,则认为该区间的每个位置都是好的。我们希望所有 n n n 位都是好的,给定 n n n k k k , 求所有可能的序列数。

某个位置的数,只需要存在一个区间使这个位置包括在内且区间为 1 ∼ k 1 \sim k 1k 排列即可。显然,最前面 k k k 个和最后面 k k k 个都是排列。

我们定义 f i f_i fi 为长度为 i i i 的序列的可能数。显然有初始值 ( ∀ i < k ) f i = 0 (\forall i < k) f_i = 0 (i<k)fi=0 f k = k ! f_k = k! fk=k! 。考虑 i > k i > k i>k f i f_i fi 的转移 : 从所有 i − j ≤ k i - j \le k ijk f j f_j fj 转移而来。显然这样会有重复转移的部分,因此我们规定:如果某个 f i f_i fi f j f_j fj 转移来,则 f i f_i fi 的倒数第二个排列就是以 j j j 结尾的。规定这样的转移有 g i − j g_{i-j} gij 种。
f i = ∑ j = i − k i − 1 f j × g i − j \displaystyle f_i = \sum_{j = i - k}^{i - 1} f_j \times g_{i - j} fi=j=iki1fj×gij
那么 g i g_i gi 的定义就是,在一个 1 ∼ k 1 \sim k 1k 的排列后加上 i i i 个数,使最后 k k k 个是一个排列,且最后一个排列和原排列之间没有别的排列的情况数。
g i = i ! − ∑ j = 1 i − 1 g j × ( i − j ) ! \displaystyle g_i = i! - \sum_{j = 1}^{i - 1} g_j \times (i - j)! gi=i!j=1i1gj×(ij)!

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int mod = 998244353;
const int N = 1e5 + 10;
ll f[N], g[N], fac[N];

int main() {
    int n, k; cin >> n >> k;
    fac[0] = 1;
    for(int i = 1; i < N; i++) 
        fac[i] = fac[i - 1] * i % mod;
    for(int i = 1; i <= k; i++) {
        g[i] = fac[i];
        for(int j = 1; j < i; j++) 
            g[i] = (g[i] - g[j] * fac[i - j] % mod + mod) % mod;
    }   
    f[k] = fac[k];
    for(int i = k + 1; i <= n; i++) {
        for(int j = 1; j <= k; j++) 
            f[i] = (f[i] + f[i - j] * g[j] % mod) % mod;
    }
    cout << f[n] << endl;
    return 0;
}

H. Meet

有一棵大小为 n n n 的树,和 m m m 个点对,每个点对中的两点都有对应的 LCA。现在让你选择一个点作为根节点,使得所有 2 m 2m 2m 个点到它们对应 LCA 的最大距离最小,输出这个数值。 n , m ≤ 1 0 5 n, m \le 10^5 n,m105

第一时间想到二分。按照数据范围,应当搞一个 O ( n l o g n ) O(nlogn) O(nlogn) 甚至 O ( n ) O(n) O(n) 的 check 方案。随便选一点作为暂时的根,注意到一些性质:

  • 不论选哪点作为根, d i s ( x , l c a ( x , y ) ) ≤ d i s ( x , y ) dis(x,lca(x,y)) \le dis(x,y) dis(x,lca(x,y))dis(x,y) ,所以检查某答案合法性时可以跳过 d i s ( x , y ) ≤ a n s dis(x,y) \le ans dis(x,y)ans ( x , y ) (x,y) (x,y) 点对。
  • 如果选择作为根的点连接到 ( x , y ) (x,y) (x,y) 的最短路径上的 u u u,那么对 ( x , y ) (x,y) (x,y) , 答案就会变为 d i s ( x , u ) dis(x,u) dis(x,u) d i s ( y , u ) dis(y,u) dis(y,u)

题解提供了一种 check 方案,对于某个答案 a n s ans ans ,若 d i s ( x , y ) ≤ a n s dis(x,y) \le ans dis(x,y)ans ,直接跳过不检查。否则,我们需要知道哪些点作为根节点可以使 x x x 点对 a n s ans ans 合法: 我们规定从 x x x y y y 的简单路径的第 a n s ans ans 步会走到点 u u u ,显然只有某个点直接连接到 x , u x,u x,u 简单路径上才能作为本题的合法根节点。如果 x x x l c a ( x , y ) lca(x,y) lca(x,y) 足够远,即 d i s ( x , l c a ( x , y ) ) > a n s dis(x,lca(x,y)) > ans dis(x,lca(x,y))>ans , 那只有 u u u 往下的点 —— 即 u u u 的子树上的点可以做根;否则,若 d i s ( x , l c a ( x , y ) ) ≤ a n s dis(x,lca(x,y)) \le ans dis(x,lca(x,y))ans ,则 , 规定从 x x x y y y 的简单路径的第 a n s + 1 ans + 1 ans+1 步会走到点 v v v , v v v 的子树上的点不能做根,否则对 x x x 而言答案超过 a n s ans ans 。最后,将所有的合法点集取交集,交集中的点就是合法的点。

由于需要维护的点集都是子树或子树取补,显然应该用 DFS 序进行区间维护。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int mod = 998244353;
const int N = 1e5 + 10;

vector<int> edge[N];
int dfsn[N], sz[N], dep[N], fa[N][20], cnt;
int x[N], y[N], dis[N], lca[N];
int pre[N], dfsdep[N], midfa[N];

void DFS(int u) {
    dfsn[u] = ++cnt, sz[u] = 1;
    for(int i : edge[u]) {
        if(dfsn[i]) continue;
        dep[i] = dep[u] + 1;
        fa[i][0] = u;
        for(int j = 1; j < 20; j++)
            fa[i][j] = fa[fa[i][j - 1]][j - 1];
        DFS(i); sz[u] += sz[i];
    }
}

int getfa(int u, int d) {
    for(int i = 19; i >= 0; i--)
        if(d & (1 << i)) u = fa[u][i];
    return u;
}

int LCA(int u, int v) {
    if(dep[u] < dep[v]) swap(u, v);
    for(int i = 19, t = dep[u] - dep[v]; i >= 0; i--)
        if(t & (1 << i)) u = fa[u][i]; 
    if(u == v) return u;
    for(int i = 19; i >= 0; i--) 
        if(fa[u][i] != fa[v][i]) u = fa[u][i], v = fa[v][i];
    return fa[u][0];
}

int main() {
    ios::sync_with_stdio(0);
    cin.tie(0), cout.tie(0);
    int n, m; cin >> n >> m;
    for(int i = 1; i < n; i++) {
        int u, v; cin >> u >> v;
        edge[u].push_back(v);
        edge[v].push_back(u);
    }
    dep[1] = 1, DFS(1);
    for(int i = 0; i < m; i++) {
        cin >> x[i] >> y[i];
        lca[i] = LCA(x[i], y[i]);
        dis[i] = dep[x[i]] + dep[y[i]] - dep[lca[i]] * 2;
    }

    int L = 0, R = n - 1, mid;
    while(L < R) {
        mid = (L + R) >> 1;
        // cout << "mid = " << mid << endl;
        int pcnt = 0, flag = 0;
        for(int i = 0; i < m; i++) {
            if(dis[i] <= mid) continue;
            if(dep[x[i]] - dep[lca[i]] > mid) {
                int u = getfa(x[i], mid);
                pre[dfsn[u]]++, pre[dfsn[u] + sz[u]]--, pcnt++;
                // printf("[%d, %d]\n", dfsn[u], dfsn[u] + sz[u] - 1);
            } else {
                int u = getfa(y[i], dis[i] - mid - 1);
                pre[1]++, pre[dfsn[u]]--, pre[dfsn[u] + sz[u]]++, pre[n + 1]--, pcnt++;
                // printf("[%d, %d]\n[%d, %d]\n", 1, dfsn[u] - 1, dfsn[u] + sz[u], n);
            }
            if(dep[y[i]] - dep[lca[i]] > mid) {
                int u = getfa(y[i], mid);
                pre[dfsn[u]]++, pre[dfsn[u] + sz[u]]--, pcnt++;
                // printf("[%d, %d]\n", dfsn[u], dfsn[u] + sz[u] - 1);
            } else {
                int u = getfa(x[i], dis[i] - mid - 1);
                pre[1]++, pre[dfsn[u]]--, pre[dfsn[u] + sz[u]]++, pre[n + 1]--, pcnt++;
                // printf("[%d, %d]\n[%d, %d]\n", 1, dfsn[u] - 1, dfsn[u] + sz[u], n);
            }
        }
        for(int i = 1, t = 0; i <= n + 1; i++) {
            t += pre[i]; if(t == pcnt) flag = 1; pre[i] = 0;
        }
        if(flag) R = mid; else L = mid + 1;
    }
    cout << L << endl;
    return 0;
}

G. Diamond

给一个长度为 n n n 的序列 a a a 。每次询问指定 l , r , p , q l, r, p, q l,r,p,q 四个参数,表示只保留下标在 [ l , r ] [l, r] [l,r] 范围内且值为 p p p 或者 q q q 的数,其它全部删除,求剩下序列的逆序数。 n ≤ 1 0 5 n \le 10^5 n105

赛场上写数据结构的队友开了这题,但感觉不像是什么常规数据结构,也根本没想到数据分治。

如果 p p p q q q 的出现次数都不超过 n \sqrt n n ,那直接 O ( n ) O(\sqrt n) O(n ) 处理每一次询问即可。对于总出现次数大于 n \sqrt n n 的颜色 x x x ,进行以下预处理:

对这种颜色单独进行一次前缀和,这个前缀和用于处理关于出现次数大于 n \sqrt n n 的颜色 x x x 和出现次数小于 n \sqrt n n 的颜色 y y y 的查询,也用于下一步的预处理。

对这种颜色与其它所有出现次数大于 n \sqrt n n 的颜色的前缀和,储存的时候只能存储 ( i n d e x , v a l ) (index, val) (index,val) 的数据对,这样单次的总复杂度是 O ( n ) O(n) O(n) 的,总复杂度 O ( n n ) O(n \sqrt n) O(nn ) 。如果采取普通的前缀和储存方法,则单次处理复杂度 O ( n n ) O(n \sqrt n) O(nn ) , 总复杂度 O ( n 2 ) O(n^2) O(n2) 。感觉说得不是很清楚,但毕竟写出来了。

感觉思路和写法不是很聪明,在 Codeforces 上提交跑出了 3 4 \frac{3}{4} 43 时空限制,应该还有优化的空间,但毕竟是过了(

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int,ll> pii;
const int N = 1e5 + 10;
const int sq = 320;
int a[N], c2n[N], cnt[N], pre[sq][N];
vector<pii> v[sq][sq];
vector<int> col[N];

// a : 原序列    cnt : 某颜色计数    c2n : 颜色转序号
// pre[i] 颜色 i 的前缀和   

int main() {
    ios::sync_with_stdio(0);
    cin.tie(0), cout.tie(0);
    int n, T; cin >> n >> T;
    for(int i = 1; i <= n; i++) 
        cin >> a[i], cnt[a[i]]++;
    for(int i = 1, j = 1; i <= n; i++) {
        if(cnt[i] > sq) {
            for(int k = 1; k <= n; k++) 
                pre[j][k] = pre[j][k - 1] + (a[k] == i);
            c2n[i] = j++;
        } 
        col[a[i]].push_back(i);
    }
    for(int i = 1; i <= n; i++) {
        // i : color
        if(c2n[i]) {
            for(int j = 1; j <= n; j++) {
                // j : index of a
                if(c2n[a[j]] && a[j] < i) {
                    vector<pii> &vec = v[c2n[i]][c2n[a[j]]];
                    if(vec.empty()) vec.push_back({0, 0});
                    vec.push_back({j, vec.back().second + pre[c2n[i]][j]});
                    // printf("v[%d][%d].push_back({%d, %d})\n", c2n[i], c2n[a[j]], vec.back().first, vec.back().second);
                }
            }
        }
    }
    while(T--) {
        int l, r, p, q; long long ans = 0;
        cin >> l >> r >> p >> q;
        if(p > q) swap(p, q);
        if(c2n[p] && c2n[q]) {
            vector<pii> &vec = v[c2n[q]][c2n[p]];
            int u1 = lower_bound(vec.begin(), vec.end(), make_pair(l, 0LL)) - vec.begin() - 1;
            int u2 = upper_bound(vec.begin(), vec.end(), make_pair(r, (ll)1e18)) - vec.begin() - 1;
            // cout << vec[u1].first << " , " << vec[u1].second << endl;
            // cout << vec[u2].first << " , " << vec[u2].second << endl;
            cout << vec[u2].second - vec[u1].second - (ll)(pre[c2n[q]][l - 1] - pre[c2n[q]][0]) * (u2 - u1) << '\n';
        } else if(c2n[q]) {
            q = c2n[q];
            for(int x : col[p]) {
                if(x > r) break;
                if(x >= l) ans += pre[q][x] - pre[q][l - 1];
            }
            cout << ans << '\n';
        } else if(c2n[p]) {
            p = c2n[p];
            for(int x : col[q]) {
                if(x > r) break;
                if(x >= l) ans += pre[p][r] - pre[p][x];
            }
            cout << ans << '\n';
        } else {
            for(int i = 0, j = 0, cnt = 0; ; ) {
                if(i == col[p].size() && j == col[q].size()) break;
                else if(i < col[p].size() && j < col[q].size()) {
                    if(col[p][i] < col[q][j]) {if(col[p][i] >= l && col[p][i] <= r) ans += cnt; i++;}
                    else {if(col[q][j] >= l && col[q][j] <= r) cnt++; j++;}
                } else if(i == col[p].size()) {if(col[q][j] >= l && col[q][j] <= r) cnt++; j++;}
                else if(j == col[q].size()) {if(col[p][i] >= l && col[p][i] <= r) ans += cnt; i++;}
            }
            cout << ans << '\n';
        }
    }
    return 0;
}

K. Tasks

有点意思的一道构造题。

以下讨论的所有区间 [ L , R ] [L, R] [L,R] 的边界均为整数。

我们称 [ L , R ] [L, R] [L,R] [ l , r ] [l, r] [l,r] 的父区间,当且仅当 [ l , r ] ⫋ [ L , R ] [l, r] \subsetneqq [L, R] [l,r][L,R] 。给定 n n n 个区间的左端点 L i L_i Li 与每个区间的烦躁值 b i b_i bi ,要求给出每个区间的右端点 R i ≥ L i R_i \ge L_i RiLi 使得每个区间的烦躁值等于所有包含它的区间的最大烦躁值 + 1 +1 +1,且任意两个区间不等。 1 ≤ n ≤ 1 0 5 1 \le n \le 10^5 1n105, 1 ≤ L i ≤ R i ≤ 1 0 6 1 \le L_i \le R_i \le 10^6 1LiRi106

注意到一些性质:

  • 若两个区间 L L L b b b 都相等,无解。
  • 若不存在 b i = 0 b_i = 0 bi=0 的区间,无解。
  • 对区间 [ l i , r i ] [l_i, r_i] [li,ri] ,若不存在区间 [ l j , r j ] [l_j, r_j] [lj,rj] 使 l j ≤ l i l_j \le l_i ljli b j = b i − 1 b_j = b_i - 1 bj=bi1 ,无解。

我们以 b i b_i bi 作为依据对区间进行分层,每一层按照左端点排序。对同一层的区间来说,从右向左扫描,产生的答案只要保证当 l i < l j l_i < l_j li<lj 时,有 r i < r j r_i < r_j ri<rj 即可。对 b ≠ 0 b \ne 0 b=0 的区间,另外还要保证层间关系:是指定的父区间的真子集。

#include <bits/stdc++.h>
using namespace std;

void fail() {
    cout << "-1\n";
    exit(0);
}

struct interval {
    int L, b, R, fa;
} a[100005];
vector<int> vec[100005];

int main() {
    ios::sync_with_stdio(0);
    cin.tie(0), cout.tie(0);
    int n; cin >> n;
    for(int i = 0; i < n; i++) {
        cin >> a[i].L >> a[i].b;
        vec[a[i].b].push_back(i);
    }
    if(vec[0].empty()) fail();
    for(int i = 0; i <= n; i++) {
        if(vec[i].size() < 2) continue;
        sort(vec[i].begin(), vec[i].end(), [](const int &A, const int &B){
            return a[A].L < a[B].L;
        });
        for(int j = 1; j < vec[i].size(); j++) {
            if(a[vec[i][j]].L == a[vec[i][j - 1]].L) fail();
        }
    }
    for(int i = 1; i <= n; i++) {
        if(vec[i].empty()) continue;
        if(vec[i - 1].empty()) fail();
        if(a[vec[i][0]].L < a[vec[i - 1][0]].L) fail();
        for(int j = 0, k = 0; k < vec[i].size(); k++) {
            while(j < vec[i - 1].size() - 1 && a[vec[i - 1][j + 1]].L <= a[vec[i][k]].L) j++;
            a[vec[i][k]].fa = vec[i - 1][j];
        }
    }

    for(int i = 0; i <= n; i++) {
        for(int j = vec[i].size() - 1, r = 1e6; j >= 0; j--) {
            int k = vec[i][j];
            if(i != 0) r = min(r, a[a[k].fa].R);
            if(a[k].L == a[a[k].fa].L && r == a[a[k].fa].R) r--;
            a[k].R = r--;
            if(a[k].L > a[k].R) fail();
        }
    }

    for(int i = 0; i < n; i++) {
        cout << a[i].R << '\n';
    }
    return 0;
}
  • 37
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值