Codeforces Round 1082 (Div. 2)A,B,C1,C2,D,E,F,G1,G2个人题解

A. 跑酷设计

#数学
今天,Alex想为Steve设计一个跑酷训练场。一个跑酷训练场是平面上整数坐标的序列 p 0 → p 1 → … → p k p_0→p_1→…→p_k p0p1pk。其中,相邻的一对坐标称为一次移动,记作 p i − 1 → p i p_{i-1}→p_i pi1pi

Alex知道Steve只能执行以下类型的移动:

  • ( x i , y i ) → ( x i + 2 , y i + 1 ) (x_i,y_i)→(x_i+2,y_i+1) (xi,yi)(xi+2,yi+1)
  • ( x i , y i ) → ( x i + 3 , y i ) (x_i,y_i)→(x_i+3,y_i) (xi,yi)(xi+3,yi)
  • ( x i , y i ) → ( x i + 4 , y i − 1 ) (x_i,y_i)→(x_i+4,y_i-1) (xi,yi)(xi+4,yi1)

注意Steve不会执行任何其他类型的移动。例如,Steve可以执行 ( 0 , 0 ) → ( 2 , 1 ) (0,0)→(2,1) (0,0)(2,1) ( 2 , 1 ) → ( 5 , 1 ) (2,1)→(5,1) (2,1)(5,1),但绝不会执行诸如 ( 2 , 1 ) → ( 3 , 2 ) (2,1)→(3,2) (2,1)(3,2) ( 3 , 0 ) → ( 5 , − 1 ) (3,0)→(5,-1) (3,0)(5,1) ( 4 , − 1 ) → ( 6 , − 1 ) (4,-1)→(6,-1) (4,1)(6,1) 的移动(即使它们看起来可能很简单)。

你被给定了平面上的一个整数坐标 ( x , y ) (x,y) (x,y)

请判断是否存在一个跑酷训练场 q 0 , q 1 , … , q k q_0,q_1,…,q_k q0,q1,,qk 满足以下条件:

  • q 0 = ( 0 , 0 ) q_0=(0,0) q0=(0,0)
  • q k = ( x , y ) q_k=(x,y) qk=(x,y)
  • 该跑酷训练场仅由Steve能够执行的移动构成。

思路

在这里插入图片描述

在草稿纸上画一下图,就可以发现非常明显的规律:
所有点都坐落在一些斜率为-1的平行线段上,这些线段所对应的直线可以表示为:
y = − x + k   ,   k % 3 = = 0   ,   k ∈ Z y=-x+k\ ,\ k\%3==0\ ,\ k\in Z y=x+k , k%3==0 , kZ
为了将直线限制为线段,再对 x x x加以限制:
x ∈ [ 2 k , 4 k ] x\in[2k,4k] x[2k,4k]
直接判断即可

代码

#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define int ll
#define rep(i,a,b) for(int i=(a);i<=(b);i++)
#define per(i,a,b) for(int i=(a);i>=(b);i--)
 
 
void solve() {
    int x, y;cin >> x >> y;
    if ((x + y) % 3 != 0)cout << "NO\n";
    else {
        int k = (x + y) / 3;
        if (x >= 2 * k && x <= 4 * k)cout << "YES\n";
        else cout << "NO\n";
    }
}
 
signed main() {
    ios::sync_with_stdio(0);
    cin.tie(0), cout.tie(0);
    int t = 1;
    cin >> t;
    while (t--)solve();
}

B. ABAB 构造

#字符串 #dp #贪心
每个测试时间限制1秒
每个测试内存限制256 MB

有一个长度为 n n n 的字符串 T T T,满足对于所有奇数 i i i T i = T_i = Ti= ‘a’;对于所有偶数 i i i T i = T_i = Ti= ‘b’。

有一天,Bob 用以下算法生成了一个字符串 S S S

初始化 S S S 为空字符串。
T T T 中移除第一个字母或最后一个字母,并将其附加到 S S S 的末尾。
如果 T T T 为空,则终止并返回字符串 S S S。否则,回到第二步。
然后,Bob 将生成的字符串 S S S 写在一张纸条上,并将其遗忘几年。当 Bob 找到这张纸条时,它已经磨损了,并且可能有人偷偷更改了一些字母。当然,Bob 想知道是否有人真的更改了这个字符串!

你被给定一个长度为 n n n 的字符串 X X X,它由 ‘a’、‘b’ 和 ‘?’ 组成。

请判断是否存在一个字符串 A A A,满足以下条件:

∣ A ∣ = n |A|=n A=n
对于所有 1 ≤ i ≤ n 1 \le i \le n 1in A i A_i Ai 是 ‘a’ 或 ‘b’;
对于所有满足 X i X_i Xi 不是 ‘?’ 的 1 ≤ i ≤ n 1 \le i \le n 1in,有 A i = X i A_i = X_i Ai=Xi
字符串 A A A 可以通过上述算法生成。

思路

本题在赛时把我给吓住了,想了特别久,最后拼尽全力写了个dp才过。。
本文先讲解一种我队友phaethon90想到很巧妙的贪心方法,再提供一种我个人的dp方法

法一:贪心

  • n n n 为偶数时, T = a b a b … a b T = abab\dots ab T=ababab。无论取左还是取右,取出的字符顺序必定是 a , b , a , b … a, b, a, b \dots a,b,a,b。因此,如果 X X X 中存在相邻位置 X i = X i + 1 X_i = X_{i+1} Xi=Xi+1(且不是 ‘?’),直接 NO
  • n n n 为奇数时, T = a b a b … a T = abab\dots a T=ababa。首项和末项都是 ‘a’。
    • 因此 S [ 0 ] S[0] S[0] 必须是 ‘a’。如果 S [ 0 ] = ′ b ′ S[0] = 'b' S[0]=b,直接 NO
    • 处理完 S [ 0 ] S[0] S[0] 后,剩下的 T T T 长度为偶数,回到了上述偶数的情况。

代码

#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define int ll
#define rep(i,a,b) for(int i=(a);i<=(b);i++)
#define per(i,a,b) for(int i=(a);i>=(b);i--)
 
 
void solve() {
    int n;cin >> n;
    string s;cin >> s;
    if ((n & 1) && s[0] == 'b')cout << "NO\n";
    else {
        for (int i = (n & 1);i < n - 1;i += 2) {
            if (s[i] == s[i + 1] && s[i] != '?') {
                cout << "NO\n";
                return;
            }
        }
        cout << "YES\n";
    }
}
 
signed main() {
    ios::sync_with_stdio(0);
    cin.tie(0), cout.tie(0);
    int t = 1;
    cin >> t;
    while (t--)solve();
}

法二:状态压缩dp

我们将 a a a视作0, b b b视作1,原串形如 0101 … 01 0101\dots 01 010101或者 0101 … 010 0101\dots 010 0101010
注意到,每次操作结束后,原串将变化为 0 … 0 , 0 … 1 , 1 … 0 , 1 … 1 0\dots 0,0 \dots 1, 1\dots 0, 1\dots 1 00,01,10,11四个状态的其中一种,因此我们用二进制数 00 , 01 , 10 , 11 00,01,10,11 00,01,10,11来表示这四种状态,分别对应着 0 , 1 , 2 , 3 0,1,2,3 0,1,2,3

![[Pasted image 20260225105335.png]]

我们可以画出这样的转移状态图,其中:
s → c s ∗ s\xrightarrow{c}s^{*} sc s
表示原串从状态 s s s的头或者尾删去一个元素可以转移到状态 s ∗ s^{*} s

状态设计:
b o o l    d p [ i ] [ m a s k ] bool\ \ dp[i][mask] bool  dp[i][mask]:是否可以利用原串生成目标串的 1 ∼ i 1\sim i 1i,并且生成完后原串状态为 m a s k mask mask
初始状态:

  • 如果 n n n是奇数,那么原串初始状态就为 0 … 0 0\dots0 00,即 d p [ 0 ] [ 00 ] = 1 dp[0][00]=1 dp[0][00]=1
  • 如果 n n n是偶数,那么原串初始状态就为 0 … 1 0\dots 1 01,即 d p [ 0 ] [ 01 ] = 1 dp[0][0 1]=1 dp[0][01]=1
    我们对状态建边,接下来遍历目标串,目标串的当前字符就可以视作为 c c c,只走边权为 c c c的路径即可实现dp的转移

代码

#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define int ll
#define rep(i,a,b) for(int i=(a);i<=(b);i++)
#define per(i,a,b) for(int i=(a);i>=(b);i--)
 
bool e[4][2][4];
 
void solve() {
    int n;cin >> n;
    string s;cin >> s;
    rep(i, 0, 3) {
        rep(j, 0, 1) {
            rep(k, 0, 3)e[i][j][k] = 0;
        }
    }
    e[0][0][1] = e[0][0][2] = 1;
    e[1][0][3] = 1;e[1][1][0] = 1;
    e[2][0][3] = 1;e[2][1][0] = 1;
    e[3][1][1] = e[3][1][2] = 1;
    vector<bool>dp(4);
    if (n & 1)dp[0] = 1;
    else dp[1] = 1;
    rep(pos, 0, n - 1) {
        vector<bool>ndp(4);
        rep(i, 0, 3) {
            if (!dp[i])continue;
            if (s[pos] == 'a') {
                rep(nxt, 0, 3) {
                    if (e[i][0][nxt])ndp[nxt] = 1;
                }
            } else if (s[pos] == 'b') {
                rep(nxt, 0, 3) {
                    if (e[i][1][nxt])ndp[nxt] = 1;
                }
            } else {
                rep(nxt, 0, 3) {
                    if (e[i][0][nxt])ndp[nxt] = 1;
                    if (e[i][1][nxt])ndp[nxt] = 1;
                }
            }
        }
        dp = ndp;
    }
    rep(i, 0, 3) {
        if (dp[i]) {
            cout << "YES\n";
            return;
        }
    }
    cout << "NO\n";
}
 
signed main() {
    ios::sync_with_stdio(0);
    cin.tie(0), cout.tie(0);
    int t = 1;
    cin >> t;
    while (t--)solve();
}

C1. 失落的文明(简单版)

#双指针
这是问题的简单版。两个版本之间的区别在于,在这个版本中,你只需为一个序列计算一个值。只有当你解决了这个问题的所有版本后,才能进行 hack。

我们定义一个生成包含 m + k m+k m+k 个整数的序列的算法如下:

首先,接收一个包含 m m m 个整数的序列 x x x 作为输入。如果 k = 0 k=0 k=0,立即终止并返回序列 x x x

然后,选择任意索引 1 ≤ i ≤ ∣ x ∣ 1 \leq i \leq |x| 1ix,并在元素 x i x_i xi 之后立即插入 ( x i + 1 ) (x_i+1) (xi+1)

如果 x x x 中恰好包含 m + k m+k m+k 个整数,则终止并返回序列 x x x。否则,返回第二步。

爱丽丝知道,一个古代文明曾使用这个算法来安全地隐藏他们的秘密。爱丽丝想了解他们想要隐藏的知识,但从算法的输出推断输入并非易事。

给定一个包含 n n n 个整数的序列 a a a,确定可以给定作为算法输入以生成 a a a 的最短序列的长度。

思路

经过观察,每个元素都有一个位于左侧、可以生成自己的最小元素,我们把这个元素叫做关键元素
以样例为例子:
9 8 9 2 3 4 4 5 3 9 ∣ 8 9 ∣ 2 3 4 4 5 3 \begin{align} \begin{array}{l} &9&&8&&9&&2&&3&&4&&4&&5&&3 \\ &9&\bigg|&8&&9&\bigg|&2&&3&&4&&4&&5&&3 \end{array} \end{align} 99 8899 223344445533
我们可以把序列拆分为三段,每段内的元素的关键元素就是这段的左端点,而左端点本身的关键元素不存在,可以记作0

我们从左到右遍历序列,将当前的关键元素记作 l a s t last last
如果发现 a [ i ] > a [ i − 1 ] + 1 a[i]>a[i-1]+1 a[i]>a[i1]+1或者 a [ i ] ≤ l a s t a[i]\leq last a[i]last,那么当前元素都不能由 l a s t last last生成,需要新开一个段
此时对关键元素为 l a s t last last的段进行结算, a n s + + ans++ ans++,更新 l a s t = a [ i ] last=a[i] last=a[i]即可

代码

#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define int ll
#define rep(i,a,b) for(int i=(a);i<=(b);i++)
#define per(i,a,b) for(int i=(a);i>=(b);i--)
 
 
void solve() {
    int n;cin >> n;
    vector<int>a(n + 1);
    a[0] = -5;
    int last = -5, ans = 0;
    rep(i, 1, n) {
        cin >> a[i];
        if (a[i] > a[i - 1] + 1 || a[i] <= last) {
            ans++;
            last = a[i];
            continue;
        }
    }
    cout << ans << '\n';
}
 
signed main() {
    ios::sync_with_stdio(0);
    cin.tie(0), cout.tie(0);
    int t = 1;
    cin >> t;
    while (t--)solve();
}

C2. 失落的文明 (困难版)

#数学

这是该问题的困难版本。两个版本的区别在于,在这个版本中,你必须计算所有子段的值之和。只有当你解决了该问题的所有版本后,才能进行 hack。

让我们定义一种生成包含 m + k m+k m+k 个整数的序列的算法如下:

  1. 首先,接收一个包含 m m m 个整数的序列 x x x 作为输入。如果 k = 0 k=0 k=0,则立即终止并返回序列 x x x
  2. 然后,选择任意索引 1 ≤ i ≤ ∣ x ∣ 1 \le i \le |x| 1ix,并在元素 x i x_i xi 之后立即插入 ( x i + 1 ) (x_i + 1) (xi+1)
  3. 如果 x x x 恰好包含 m + k m+k m+k 个整数,则终止并返回序列 x x x。否则,返回第二步。

Alice 知道这个算法被一个古老文明用来安全地隐藏他们的秘密。Alice 想要学习他们想要隐藏的知识,但从算法的输出推断输入并不是一件容易的事。

对于一个包含 n n n 个整数的序列 b b b,我们定义 f ( b ) f(b) f(b) 为:能够通过该算法生成 b b b 的最短输入序列的长度。

给定一个包含 n n n 个整数的序列 a a a,请计算以下各项之和的值:

∑ l = 1 n ∑ r = l n f ( [ a l , a l + 1 , … , a r ] ) \sum_{l=1}^n \sum_{r=l}^n f([a_l, a_{l+1}, \dots, a_r]) l=1nr=lnf([al,al+1,,ar])

换句话说,你必须找到 a a a 的所有子段 ∗ ^* c c c f ( c ) f(c) f(c) 之和。

思路

依旧以样例作为例子:
![[Pasted image 20260225113257.png]]

区别于上一题的关键元素,我们考虑能够生成这个元素的最近的元素,叫做最近关键元素,元素 a [ i ] a[i] a[i]的最近关键元素记作 L [ i ] L[i] L[i]

那么如何预处理 L [ i ] L[i] L[i]呢?
可以使用 s e t < p a i r < i n t , i n t > > set<pair<int,int>> set<pair<int,int>>来存储 { 元素的值 , 元素下标 } \{ 元素的值 ,元素下标 \} {元素的值,元素下标},用于维护当前段内的元素信息
如果新增的元素是属于当前集合的,那么就可以 l o w e r _ b o u n d lower\_bound lower_bound找到最近的生成元素,满足 a [ p o s ] < a [ i ] a[pos]<a[i] a[pos]<a[i]并且 p o s < i pos<i pos<i
如果新增元素已经不属于当前集合,那么就清空集合,开一个新的段

为了计算所有合法的区间,我们先倒序遍历右端点,在固定右端点的情况下枚举左端点
我们把所有情况计算的答案打表:
![[Pasted image 20260225115055.png]]

观察到: r r r在转移的时候,将会把 L [ r ] + 1 ∼ r L[r]+1\sim r L[r]+1r这一段区间的答案全部减一
因此,我们可以将每个元素的贡献视作一个个区间,采用分贡献的方式来计算答案
r r r n n n遍历到1的这 n n n个时刻中,区间 [ L [ i ] + 1 , i ] [L[i]+1,i] [L[i]+1,i]的贡献将在 1 ≤ r ≤ i 1\leq r\leq i 1ri的时刻对答案有贡献,在 i < r ≤ n i<r\leq n i<rn的时刻没有贡献
这就像把每个区间比喻成灯泡,一开始全都亮着,随着时间的推移依次慢慢关闭,那么总贡献实际上就是每个灯泡的单位时间贡献乘以亮着的时间,也就是区间长度乘以留存时间
a n s = ∑ i = 1 n ( n − i + 1 ) × ( i − L [ i ] ) ans=\sum_{i=1}^{n}{(n-i+1)\times (i-L[i])} ans=i=1n(ni+1)×(iL[i])

代码

#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define int ll
#define rep(i,a,b) for(int i=(a);i<=(b);i++)
#define per(i,a,b) for(int i=(a);i>=(b);i--)
 
void solve() {
    int n;cin >> n;
    vector<int>a(n + 1);
    vector<int>l(n + 1);
    a[0] = -5;
    int last = -5, ans = 0;
    set<pair<int, int>>s;//val,id
    rep(i, 1, n) {
        cin >> a[i];
        if (a[i] > a[i - 1] + 1 || a[i] <= last) {
            last = a[i];
            s.clear();
            l[i] = 0;
        } else {
            auto it = s.lower_bound({ a[i],0 });
            it--;
            l[i] = it->second;
        }
        s.insert({ a[i],i });
    }
    rep(i, 1, n) {
        ans += (n - i + 1) * (i - l[i]);
    }
    cout << ans << '\n';
}
 
signed main() {
    ios::sync_with_stdio(0);
    cin.tie(0), cout.tie(0);
    int t = 1;
    cin >> t;
    while (t--)solve();
}

D. 记忆数字

#数学 #构造

2 n 2n 2n 张卡片,上面写着数字 1 , 1 , 2 , 2 , … , n , n 1, 1, 2, 2, \dots, n, n 1,1,2,2,,n,n。换句话说,对于所有 j = 1 , 2 , … , n j=1, 2, \dots, n j=1,2,,n,都恰好有 2 2 2 张写着数字 j j j 的卡片。每张卡片的正面只写有一个数字。

你将玩一个翻牌游戏。最初,所有 2 n 2n 2n 张卡片都背面朝上(没有数字的一面)放置。在每一轮中,你恰好翻开两张卡片。如果这两张卡片的数字相同,你就丢弃这两张卡片;否则,你将它们翻回原来的位置。当所有 2 n 2n 2n 张卡片都被丢弃时,你就赢了。请注意,你不需要同时翻开两张卡片,因此你可以在看到第一张卡片的数字后,再决定第二张卡片的选择。

考虑以下用于玩游戏的“贪心”算法。最初, 2 n 2n 2n 张卡片被随机排列成一排。然后你在每一轮的策略如下:

  1. 如果有两张你之前翻开过且数字相同的卡片,翻开这两张卡片。
  2. 否则,翻开目前为止第一张 ∗ ^* 从未被翻开过的卡片。假设这张卡片上的数字是 x x x
  3. 随后,如果还有另一张你之前翻开过且数字为 x x x 的卡片,翻开那张卡片。
  4. 否则,翻开目前为止(包括本轮)第一张 ∗ ^* 从未被翻开过的卡片作为第二张。

可以证明,该算法的策略在每一轮都是唯一确定的。

你必须解决关于上述算法的以下问题:

给定 n n n k k k,请找到一种 2 n 2n 2n 张卡片的排列方式,使得上述算法恰好需要 k k k 轮才能赢得游戏。
此外,如果不存在这样的排列方式,请报告。

思路

先考虑最少的轮数和最多的轮数
如果按照 1 , 1 , 2 , 2 , 3 , 3 , … , n , n 1,1,2,2,3,3,\dots ,n,n 1,1,2,2,3,3,,n,n的方式进行排列,此时轮数最少,答案为 n n n
如果按照 2 , 1 , 3 , 2 , 4 , 3 , … , n , n − 1 , n , 1 2,1,3,2,4,3,\dots ,n,n-1,n,1 2,1,3,2,4,3,,n,n1,n,1排列,此时轮数最多,答案为 n + n − 1 n+n-1 n+n1
为什么这样排轮数最多呢?
对于每个数字,选取与他相同的数字必定会消耗一轮,也就是消耗 n n n轮,剩余的操作都是多余的操作,我们需要尽可能最大化多余的操作数
因此必定不能让玩家轻易地选到两个相同的
按照上述方法排列,每次必须要翻开两个新的卡才会发现一对相同的(1次多余操作),并且翻新卡的操作与选取相同的操作互不影响,则两两选会多出n-1次多余操作,这样能够使得多余操作最大化

因此可以先对 k k k的范围进行判断, n ≤ k ≤ 2 n − 1 n\leq k\leq 2n-1 nk2n1
接下来需要想办法控制多余操作次数

观察到,如果记 f ( n ) f(n) f(n)为最大化操作次数后长度为 n n n的序列,那么 f ( n ) , f ( n − 1 ) , f ( n − 2 ) … f(n),f(n-1),f(n-2)\dots f(n),f(n1),f(n2)存在递推,他们的前缀是相同的
比如:
f ( 4 ) = { 2 , 1 , 3 , 2 , 4 , 3 , 4 , 1 } f ( 5 ) = { 2 , 1 , 3 , 2 , 4 , 3 , 5 , 4 , 5 , 1 } f ( 6 ) = { 2 , 1 , 3 , 2 , 4 , 3 , 5 , 4 , 6 , 5 , 6 , 1 } \begin{align} &f(4)=\{ 2,1,3,2,4,3,4,1 \}\\ \\ &f(5)=\{ 2,1,3,2,4,3,5,4,5,1 \}\\ \\ &f(6)=\{ 2,1,3,2,4,3,5,4,6,5,6,1 \} \end{align} f(4)={2,1,3,2,4,3,4,1}f(5)={2,1,3,2,4,3,5,4,5,1}f(6)={2,1,3,2,4,3,5,4,6,5,6,1}
那么,如果想要在 n = 6 n=6 n=6的时候只多操作3次,那么其实等价于在 f ( 4 ) f(4) f(4)的基础上最小化操作次数,因为 f ( 4 ) f(4) f(4)的多余操作数为3:
{ 2 , 1 , 3 , 2 , 4 , 3 , 4 , 1 , 5 , 5 , 6 , 6 } \{ 2,1,3,2,4,3,4,1,5,5,6,6 \} {2,1,3,2,4,3,4,1,5,5,6,6}
因此我们可以通过改变序列前缀的方式来控制多余操作次数,后缀直接将两个相同元素放一起就行

代码

#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define int ll
#define rep(i,a,b) for(int i=(a);i<=(b);i++)
#define per(i,a,b) for(int i=(a);i>=(b);i--)
 
void solve() {
    int n, k;cin >> n >> k;
    if (k < n || k>2 * n - 1) {
        cout << "NO\n";return;
    }
    cout << "YES\n";
    int cnt = k - n;
    vector<int>ans(2*n + 1);
    rep(i, 1, cnt) {
        ans[2 * i] = i;
        ans[2 * i - 1] = i + 1;
    }
    ans[2 * (cnt + 1)] = 1;
    ans[2 * (cnt + 1) - 1] = cnt + 1;
    rep(i, cnt + 2, n) {
        ans[2 * i] = ans[2 * i - 1] = i;
    }
    rep(i, 1, 2 * n)cout << ans[i] << " ";cout << '\n';
}
 
signed main() {
    ios::sync_with_stdio(0);
    cin.tie(0), cout.tie(0);
    int t = 1;
    cin >> t;
    while (t--)solve();
}

ps:E、F都是我看官方题解补的题,思路与官方一致

E. 操纵的括号序列

#字符串 #dp #计数DP
每个测试点的时限:2.0 秒
每个测试点的内存限制:256 MB

合法括号序列是指由 ‘(’ 和 ‘)’ 组成的序列,通过在序列中插入任意数量的 ‘1’ 和 ‘+’,可以将其转换为合法的数学表达式。例如,序列 “()(()())” 是合法括号序列,而 “())(()” 或 “(()” 则不是。

给定一个合法括号序列 S S S

让我们考虑将一个子序列 ∗ ^* 向右位移。形式上,当一个子序列 S i 1 S i 2 … S i k S_{i_1}S_{i_2} \dots S_{i_k} Si1Si2Sik 向右位移时,所选下标处的字符将同时按以下方式重新赋值:

S i 1 ← S i k S_{i_1} \leftarrow S_{i_k} Si1Sik
S i 2 ← S i 1 S_{i_2} \leftarrow S_{i_1} Si2Si1
S i 3 ← S i 2 S_{i_3} \leftarrow S_{i_2} Si3Si2
… \dots
S i k ← S i k − 1 S_{i_k} \leftarrow S_{i_{k-1}} SikSik1

换句话说,第 j j j 个选定下标处的元素会被重新赋值为位移前第 ( ( j − 2 )   m o d   k + 1 ) ((j-2) \bmod k + 1) ((j2)modk+1) 个选定下标处的字符。

例如,当 S S S 为 “()(()())” 时,位移子序列 S 2 S 4 S_2 S_4 S2S4 会使 S S S 变为 “((())())”。另一方面,位移 S 2 S 3 S 5 S_2 S_3 S_5 S2S3S5 会使 S S S 变为 “())((())”。

请计算有多少个非空子序列,使得 S S S 在向右位移后仍然是一个合法括号序列。由于答案可能很大,你只需要输出答案对 998244353 998244353 998244353 取模后的结果。

思路

对于一个括号串,如何判断它是否是合法的?
( ( (视作 + 1 +1 +1 ) ) )视作 − 1 -1 1,如果该序列所有位置前缀和的值都 ≥ 0 \geq 0 0,那么这就是一个合法的括号串

p r e [ i ] pre[i] pre[i] 1 ∼ i 1\sim i 1i的前缀和
假设我们选取了 s k 1 , s k 2 , … s k t s_{k_{1}},s_{k_{2}},\dots s_{k_{t}} sk1,sk2,skt作为子串,并进行一次右移操作,发现:

  • 1 ≤ i < k 1 1\leq i<k_{1} 1i<k1时, p r e [ i ] pre[i] pre[i]的值不会发生改变,由于原串是合法串,所以 1 ∼ k 1 1\sim k_{1} 1k1必定合法
  • k t < i ≤ n k_{t}<i\leq n kt<in时, p r e [ i ] pre[i] pre[i]的值不会发生改变,由于原串是合法串,所以 k t ∼ n k_{t}\sim n ktn必定合法
    所以我们讨论 i = k j i=k_{j} i=kj的情况,对于 p r e [ k j ] pre[k_{j}] pre[kj],在右移后他将变为 p r e [ k j ] − s k j + s k t pre[k_{j}]-s_{k_{j}}+s_{k_{t}} pre[kj]skj+skt
  • 如果 s k t = = + 1 s_{k_{t}}==+1 skt==+1,那么变化量 − s k j + s k t = 1 − s k j ≥ 0 -s_{k_{j}}+s_{k_{t}}=1-s_{k_{j}}\geq 0 skj+skt=1skj0,对前缀和的合法性没有影响
  • 如果 s k t = = − 1 s_{k_{t}}==-1 skt==1,那么变化量 − s k j + s k t = − 1 − s k j ≤ 0 -s_{k_{j}}+s_{k_{t}}=-1-s_{k_{j}}\leq 0 skj+skt=1skj0
    • 如果 s k j = = − 1 s_{k_{j}}==-1 skj==1,那么变化量 − s k j + s k t = − 1 + 1 = 0 -s_{k_{j}}+s_{k_{t}}=-1+1= 0 skj+skt=1+1=0,对前缀和的合法性没有影响
    • 如果 s k j = = 1 s_{k_{j}}==1 skj==1,那么变化量 − s k j + s k t = − 1 − 1 = − 2 -s_{k_{j}}+s_{k_{t}}=-1-1= -2 skj+skt=11=2,这就要求 p r e [ s k j ] ≥ 2 pre[s_{k_{j}}]\geq 2 pre[skj]2

因此,如果 s k t = = + 1 s_{k_{t}}==+1 skt==+1,那么前面的子序列怎么选都可以,那么每个 j < i j<i j<i的位置就两种状态,选与不选,方案数为 2 i − 1 2^{i-1} 2i1
如果 s k t = = − 1 s_{k_{t}}==-1 skt==1,那么我们需要对 s j = = + 1 s_{j}==+1 sj==+1的位置进行特殊考虑
状态设计:
d p [ i ] dp[i] dp[i]表示 1 ∼ i 1\sim i 1i中,多连接一个 s k t = − 1 s_{k_{t}}=-1 skt=1后仍然合法的子序列数量
状态转移:
设 S i 满足 : min ⁡ j ∈ S i { p r e [ j ] } ≥ 2 ∀ j ∈ S i    ⟹    j < i   ,   s j = + 1 \begin{align} &设S_{i}满足:\\ \\ &\min_{j\in S_{i}}\{ pre[j] \}\geq 2\\ \\ &\forall j\in S_{i}\implies j<i\ ,\ s_{j}=+1 \end{align} Si满足:jSimin{pre[j]}2jSij<i , sj=+1
d p [ i ] = 1 + ∑ j ∈ S i d p [ j ] + ∑ j < i   ,   s j = = − 1 d p [ j ] dp[i]=1+\sum_{j\in S_{i}}dp[j]+\sum_{j<i\ ,\ s_{j}==-1}dp[j] dp[i]=1+jSidp[j]+j<i , sj==1dp[j]

  • 多加一个1代表子序列长度为1的时候也合法
  • 所有 s j = − 1 s_{j}=-1 sj=1的位置都必须有 p r e [ j ] ≥ 2 pre[j]\geq 2 pre[j]2
  • s j = = + 1 s_{j}==+1 sj==+1的位置没有限制

为了快速得到 j ∈ S i j\in S_{i} jSi,设 L [ i ] L[i] L[i] i i i左侧距离 i i i最近的 p r e [ p o s ] < 2 pre[pos]<2 pre[pos]<2的位置 p o s pos pos
∑ j ∈ S i d p [ j ] \sum_{j\in S_{i}}dp[j] jSidp[j]可以转化为 ∑ L [ i − 1 ] < j < i   ,   s j = = − 1 d p [ j ] \sum_{L[i-1]<j<i\ ,\ s_{j}==-1}dp[j] L[i1]<j<i , sj==1dp[j]

为了快速得到 s j = = + 1 s_{j}==+1 sj==+1 s j = = − 1 s_{j}==-1 sj==1位置的dp值之和,设 p r e l [ i ] = ∑ s j = = + 1   ,   j ≤ i d p [ j ] pre_{l}[i]=\sum_{s_{j}==+1\ ,\ j\leq i}dp[j] prel[i]=sj==+1 , jidp[j] p r e r [ i ] = ∑ s j = = − 1   ,   j ≤ i d p [ j ] pre_{r}[i]=\sum_{s_{j}==-1\ ,\ j\leq i}dp[j] prer[i]=sj==1 , jidp[j]

则转移可以优化为:
d p [ i ] = 1 + p r e l [ i − 1 ] − p r e l [ L [ i − 1 ] ] + p r e r [ i − 1 ] dp[i]=1+pre_{l}[i-1]-pre_{l}[L[i-1]]+pre_{r}[i-1] dp[i]=1+prel[i1]prel[L[i1]]+prer[i1]
最后我们可以在递推的时候算答案:

  • 如果 s i = = + 1 s_{i}==+1 si==+1,此时没有限制, a n s + = 2 i − 1 ans+=2^{i-1} ans+=2i1
  • 如果 s i = = − 1 s_{i}==-1 si==1,此时使用dp值, a n s + = d p [ i ] ans+=dp[i] ans+=dp[i]

注意dp中有减法,取模要先加模

代码

#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define int ll
#define rep(i,a,b) for(int i=(a);i<=(b);i++)
#define per(i,a,b) for(int i=(a);i>=(b);i--)
 
const int mod = 998244353;
int n;
 
constexpr int qpow(int a, int b) {
    int ans = 1;a %= mod;
    while (b) {
        if (b & 1) { ans *= a; ans %= mod; }
        a *= a;a %= mod;b >>= 1;
    }
    return ans % mod;
}
 
void solve() {
    cin >> n;
    string s;cin >> s;
    vector<int>prel(n + 1);
    vector<int>prer(n + 1);
    vector<int>dp(n + 1);
    vector<int>pre(n + 1);
    vector<int>L(n + 1);
    rep(i, 1, n) {
        int add = 0;
        if (s[i - 1] == '(')add = 1;
        else add = -1;
        pre[i] = add;
        if (i - 1 >= 1) {
            pre[i] += pre[i - 1];
            L[i] = L[i - 1];
        }
        if (pre[i] < 2)L[i] = i;
    }
    int ans = 0;
    rep(i, 1, n) {
        dp[i] = 1 + prel[i - 1] - prel[L[i - 1]] + prer[i - 1] + mod;
        dp[i] %= mod;
        if (s[i - 1] == '(')prel[i] = dp[i];
        else prer[i] = dp[i];
        if (i - 1 >= 1) {
            prel[i] += prel[i - 1];
            prer[i] += prer[i - 1];
            prel[i] %= mod;
            prer[i] %= mod;
        }
        if (s[i - 1] == '(') {
            ans += qpow(2, i - 1);
            ans %= mod;
        } else {
            ans += dp[i];
            ans %= mod;
        }
    }
    cout << ans << '\n';
}
 
signed main() {
    ios::sync_with_stdio(0);
    cin.tie(0), cout.tie(0);
    int t = 1;
    cin >> t;
    while (t--)solve();
}

F. 非二分查找与查询

#STL #数学
对于一个由 m m m 个整数组成的序列 b b b,集合 S ( b ) S(b) S(b) 被定义为满足以下条件的元组 ( i , j , k ) (i, j, k) (i,j,k) 的集合:

  • i , j , k i, j, k i,j,k 是整数;
  • 1 ≤ k < m 1 \le k < m 1k<m
  • 1 ≤ i < j ≤ m − k + 1 1 \le i < j \le m - k + 1 1i<jmk+1
  • 对于 b b b 中的每一个元素 v v v v v v [ b i , b i + 1 , … , b i + k − 1 ] [b_i, b_{i+1}, \dots, b_{i+k-1}] [bi,bi+1,,bi+k1] [ b j , b j + 1 , … , b j + k − 1 ] [b_j, b_{j+1}, \dots, b_{j+k-1}] [bj,bj+1,,bj+k1] 中出现的次数相同。

例如,当 b = [ 1 , 2 , 1 , 2 ] b = [1, 2, 1, 2] b=[1,2,1,2] 时,元组 ( 1 , 3 , 2 ) (1, 3, 2) (1,3,2) S ( b ) S(b) S(b) 的一个元素,因为 1 1 1 2 2 2 [ b 1 , b 2 ] [b_1, b_2] [b1,b2] [ b 3 , b 4 ] [b_3, b_4] [b3,b4] 中都恰好各出现了一次。

此外,我们在正整数序列上定义了两个函数:

  • k m a x ( b ) k_{max}(b) kmax(b) 定义为 S ( b ) S(b) S(b) 中所有元素 ( i , j , k ) (i, j, k) (i,j,k) k k k 的最大值;
  • f ( b ) f(b) f(b) 定义为 S ( b ) S(b) S(b) 中满足 k = k m a x ( b ) k = k_{max}(b) k=kmax(b) 的不同元素 ( i , j , k ) (i, j, k) (i,j,k) 的数量。

特别地,当集合 S ( b ) S(b) S(b) 为空时,定义 k m a x ( b ) = 0 k_{max}(b) = 0 kmax(b)=0 f ( b ) = 0 f(b) = 0 f(b)=0

给你一个包含 n n n 个整数的序列 a a a。请回答 q q q 个如下类型的查询:

  • i   x i \ x i x:将 a i a_i ai 的值修改为 x x x。然后,求出 k m a x ( a ) k_{max}(a) kmax(a) f ( a ) f(a) f(a) 的值。

请注意,更新是持久的。换句话说,一个查询中的修改会影响后续的所有查询。

思路

本题的破题点非常巧妙: k m a x k_{max} kmax必定为序列中相同元素下标之差的最大值
比如样例1的时刻2:
1 2 2 1 5 \begin{align} \begin{array} {l} &1&2&2&1&5 \end{array} \end{align} 12215
两个元素1的下标之差为3, k m a x = 3 k_{max}=3 kmax=3,为什么呢?
形如 x p , a , b , … , z , x q x_{p},a,b, \dots ,z,x_{q} xp,a,b,,z,xq的部分,必定可以选取区间 [ p , q − 1 ] [p,q-1] [p,q1] [ p + 1 , q ] [p+1,q] [p+1,q]这两个区间,出现的元素数量必定一致。因此,找到相同元素相隔最远的距离即为 k m a x k_{max} kmax

在确定 k m a x ( a ) k_{max}(a) kmax(a)后如何求 f ( a ) f(a) f(a)呢?
继续以样例1为例,取时刻3:
1 2 2 1 2 \begin{align} \begin{array} {l} &1&2&2&1&2 \end{array} \end{align} 12212
显然 k m a x = 3 k_{max}=3 kmax=3,无论是1还是2,下标差值最大都为3
因此,我们现在区间的长度已经固定为3,找出合法的区间起点对 i , j i,j i,j即可
f i r s t [ x ] first[x] first[x]为元素 x x x第一次出现的位置, f i r s t [ 1 ] = 1 , f i r s t [ 2 ] = 2 first[1]=1,first[2]=2 first[1]=1,first[2]=2
此时, i , j i,j i,j可以在 [ 1 , 3 ] [1,3] [1,3]中任选,答案为 C 3 2 = 3 C_{3}^{2}=3 C32=3
为什么是 [ 1 , 3 ] [1,3] [1,3]
我们知道,形如 x p , a , b , … , z , x q x_{p},a,b, \dots ,z,x_{q} xp,a,b,,z,xq的部分,必定可以选取区间 [ p , q − 1 ] [p,q-1] [p,q1] [ p + 1 , q ] [p+1,q] [p+1,q]这两个区间,而我们已经确定了区间长度,只需要确定区间起点即可确定一个区间,所以对于元素 x x x而言,区间起点在 [ p , p + 1 ] [p,p+1] [p,p+1]中选择即可

那么形如 x p , y p + 1 , … x_{p},y_{p+1},\dots xp,yp+1,的部分呢?(假设元素 x , y x,y x,y的最大差值都为 k m a x k_{max} kmax

  • x x x而言,可以在 [ p , p + 1 ] [p,p+1] [p,p+1]选择
  • y y y而言,可以在 [ p + 1 , p + 2 ] [p+1,p+2] [p+1,p+2]选择
  • 两个区间可以合并为 [ p , p + 2 ] [p,p+2] [p,p+2]
    因此,只要在 [ p , p + 2 ] [p,p+2] [p,p+2]中任选两个作为区间起点,这两个区间必定合法

l e n [ x ] len[x] len[x]为元素 x x x的下标最大差值
因此,假如 ∀ x ∈ S → l e n [ x ] = k m a x \forall x\in S\to len[x]=k_{max} xSlen[x]=kmax,当 a i , a i + 1 , … , a i + t ∈ S a_{i},a_{i+1},\dots,a_{i+t}\in S ai,ai+1,,ai+tS时,区间合并后为 [ p , p + t + 1 ] [p,p+t+1] [p,p+t+1],对答案的贡献为 C t + 1 2 C_{t+1}^{2} Ct+12
因此,我们只需要关注所有 l e n [ x ] = = k m a x len[x]==k_{max} len[x]==kmax的元素的 f i r s t [ x ] first[x] first[x],以及他们是否连续,将连续的段进行区间合并,每个区间由一个个连续的 f i r s t [ i ] first[i] first[i]组成,计算每个区间对答案的贡献即可

本题的另一大难点就在于,如何动态地维护这些连续的区间
我们使用 s e t < p a i r < i n t , i n t > > l r [ N ] set<pair<int,int>>lr[N] set<pair<int,int>>lr[N]来存储区间,其中 l r [ i ] lr[i] lr[i]存放 l e n = i len=i len=i的区间
每次将位置 i i i的值修改为 x x x的时候,需要先将原先的信息 o r i g i n a l _ l e n , o r i g i n a l _ v a l original\_len,original\_val original_len,original_val所对应的区间信息进行修改
修改完后再将新的信息 l e n , v a l len,val len,val更新进对应的区间信息中

a n s [ k ] ans[k] ans[k]表示 l r [ k ] lr [k] lr[k]中所有区间的贡献

  • 函数 d e l ( k , x ) del(k,x) del(k,x)用于将 l r [ k ] lr [k] lr[k]中包含 x x x的区间 [ L , R ] [L,R] [L,R]分裂为 [ L , x − 1 ] , [ x + 1 , R ] [L,x-1],[x+1,R] [L,x1],[x+1,R],并且需要考虑无法分裂的种种情况
  • 函数 a d d ( k , x ) add(k,x) add(k,x)用于在 l r [ k ] lr [k] lr[k]中将 [ L , x − 1 ] , [ x + 1 , R ] [L,x-1],[x+1,R] [L,x1],[x+1,R]合并为 [ L , R ] [L,R] [L,R],并且需要考虑无法合并的种种情况
  • d e l del del a d d add add的过程中,动态更新 a n s [ k ] ans[k] ans[k]

此外,创建一个 m u l t i s e t < i n t > l e n multiset<int>len multiset<int>len用于维护当前的最大下标差值,用于确定 k m a x k_{max} kmax
最后输出 k m a x , a n s [ k m a x ] k_{max},ans[k_{max}] kmax,ans[kmax]即可

写代码的时候需要多注意集合的判空,否则非常容易越界!

代码

#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define int ll
#define rep(i,a,b) for(int i=(a);i<=(b);i++)
#define per(i,a,b) for(int i=(a);i>=(b);i--)
 
const int N = 2e5 + 5, Q = 1e5 + 5;
int a[N], n, q, ans[N];
set<pair<int, int>>lr[N];
 
constexpr int C(int x) {
    return x * (x - 1) / 2;
}
 
void add(int k, int x) {
    if (k <= 0)return;
    int L = x, R = x;
    auto& s = lr[k];
    auto it = s.lower_bound({ x,0 });
    if (it != s.end() && it->first == x + 1) {
        auto [l, r] = *it;
        R = r;
        ans[k] -= C(r - l + 2);
        s.erase(it);
    }
    it = s.lower_bound({ x,0 });
    if (it != s.begin() && (--it)->second == x - 1) {
        auto [l, r] = *it;
        L = l;
        ans[k] -= C(r - l + 2);
        s.erase(it);
    }
    s.insert({ L,R });
    ans[k] += C(R - L + 2);
}
 
void del(int k, int x) {
    if (k <= 0)return;
    auto& s = lr[k];
    auto it = s.lower_bound({ x + 1,0 });
    if (it != s.begin()) {
        it--;
        auto [L, R] = *it;
        if (L > x || R < x)return;
        s.erase(it);
        ans[k] -= C(R - L + 2);
        if (x - 1 >= L) {
            s.insert({ L,x - 1 });
            ans[k] += C(x - 1 - L + 2);
        }
        if (x + 1 <= R) {
            s.insert({ x + 1,R });
            ans[k] += C(R - (x + 1) + 2);
        }
    }
}
 
void solve() {
    cin >> n >> q;
    vector<set<int>>pos(n + 1);
    rep(i, 1, n) {
        cin >> a[i];
        pos[a[i]].insert(i);
        lr[i].clear();
        ans[i] = 0;
    }
    lr[0].clear();ans[0] = 0;
    multiset<int>len;
    rep(i, 1, n) {
        if (pos[i].size()) {
            int k = *pos[i].rbegin() - *pos[i].begin();
            len.insert(k);
            add(k, *pos[i].begin());
        }
    }
    // cout << "HERE" << endl;
    rep(i, 1, q) {
        int x, val;cin >> x >> val;
        if (!len.size()) {
            cout << "0 0\n";continue;
        }
        int orilen = *pos[a[x]].rbegin() - *pos[a[x]].begin();
        len.erase(len.find(orilen));
        del(orilen, *pos[a[x]].begin());
        pos[a[x]].erase(x);
        if (pos[a[x]].size()) {
            int nowlen = *pos[a[x]].rbegin() - *pos[a[x]].begin();
            len.insert(nowlen);
            add(nowlen, *pos[a[x]].begin());
        }
        a[x] = val;
 
        if (pos[a[x]].size()) {
            orilen = *pos[a[x]].rbegin() - *pos[a[x]].begin();
            len.erase(len.find(orilen));
            del(orilen, *pos[a[x]].begin());
        }
        pos[a[x]].insert(x);
        int nowlen = *pos[a[x]].rbegin() - *pos[a[x]].begin();
        len.insert(nowlen);
        add(nowlen, *pos[a[x]].begin());
 
        int ma = *len.rbegin();
        if (!ma)cout << "0 0\n";
        else cout << ma << " " << ans[ma] << '\n';
    }
}
 
signed main() {
    ios::sync_with_stdio(0);
    cin.tie(0), cout.tie(0);
    int t = 1;
    cin >> t;
    while (t--)solve();
}

G1. 单调黑白矩阵 (简单版)

#bitset #数学 #STL

这是该问题的简单版本。两个版本的区别在于,在这个版本中, n n n q q q 的限制较小。只有当且仅当你解决了该问题的所有版本后,才能进行 hack。

一个大小为 n × n n \times n n×n 的黑白矩阵是指一个具有 n n n 行和 n n n 列的矩阵,其中每个单元格都被涂成了黑色或白色。令黑白矩阵 C C C 中单元格 ( r , c ) (r, c) (r,c) 的颜色表示为 C [ r , c ] C[r, c] C[r,c]

如果一个矩阵 C C C 满足以下条件,我们就称其为单调的:

不存在两行 1 ≤ i < j ≤ n 1 \le i < j \le n 1i<jn 和两列 1 ≤ k < l ≤ n 1 \le k < l \le n 1k<ln 满足以下三个条件:

  1. C [ i , k ] = C [ j , l ] C[i, k] = C[j, l] C[i,k]=C[j,l]
  2. C [ j , k ] = C [ i , l ] C[j, k] = C[i, l] C[j,k]=C[i,l]
  3. C [ i , k ] ≠ C [ j , k ] C[i, k] \neq C[j, k] C[i,k]=C[j,k]

现有一个大小为 n × n n \times n n×n 的黑白矩阵 M M M,最初所有单元格均为白色。请处理 q q q 个如下类型的查询:

  • r   c r \ c r c:将矩阵 M M M 中单元格 ( r , c ) (r, c) (r,c) 的颜色修改为黑色。然后,判断 M M M 是否为单调矩阵。

对于每个查询,保证单元格 ( r , c ) (r, c) (r,c) 在查询前的颜色为白色。

请注意,更新是持久的。换句话说,一个查询中的颜色修改会影响后续的所有查询。

思路

我们将白色视作 0 0 0,黑色视作 1 1 1,那么整个矩阵就可以变为 n n n行二进制数
如果存在矩阵不单调的情况,那么必然出现下面的两种情况之一:
1 … 0 0 … 1 0 … 1 1 … 0 \begin{align} \begin{array} {l}&1&\dots&0 \\ &0&\dots&1 \end{array}\quad \begin{array} {l}&0&\dots&1 \\ &1&\dots&0 \end{array}\end{align} 10010110
如果矩阵单调,那么任取四个数字必然是下面的情况:
KaTeX parse error: Unknown column alignment: m at position 31: …\begin{array} {m̲}&0&\dots&0 \\ …
或者是两行位置互换,共8种可能

结论:矩阵单调    ⟺    \iff 任意两行 a , b a, b a,b 满足包含关系( a ⊆ b a \subseteq b ab b ⊆ a b \subseteq a ba)。
用位运算表示: ( a   & ∼ b ) = = 0 (a \ \& \sim b) == 0 (a &b)==0 ( ∼ a   &   b ) = = 0 (\sim a \ \& \ b) == 0 (a & b)==0

我们记 p d ( a , b ) = ( a & ∼ b ≠ 0 ∣ ∣ ∼ a & b ≠ 0 ) pd(a,b)=(a\&\sim b\neq0||\sim a\&b\neq0) pd(a,b)=(a&b=0∣∣a&b=0)
因此,只要存在两行 a , b a,b a,b使得 p d ( a , b ) = 1 pd(a,b)=1 pd(a,b)=1,那么必然不单调

为了快速判断是否存在这样的 a , b a,b a,b,我们将上述 p d pd pd的这种双向关系改为单向的偏序关系, p d ( a , b ) = ( a & ∼ b ≠ 0 ) pd(a,b)=(a\&\sim b\neq 0) pd(a,b)=(a&b=0),如果 p d ( a , b ) pd(a,b) pd(a,b),则 a ⪯ b a\preceq b ab

为了快速判断,我们将行按黑格子个数 cnt 排序,在单调矩阵中,排序后的行必须构成一个“包含链”: S p 1 ⊆ S p 2 ⊆ ⋯ ⊆ S p n S_{p_1} \subseteq S_{p_2} \subseteq \dots \subseteq S_{p_n} Sp1Sp2Spn

也就是说,假设现在有一个新的行状态 x x x,排序后位于 { … , p , x , q , …   } \{ \dots,p,x,q, \dots \} {,p,x,q,}的位置,而 p ⪯ q p\preceq q pq,因此只要 p ⪯ x p\preceq x px并且 x ⪯ q x\preceq q xq,那么就不会破坏单调性

因此,我们可以开一个 s e t set set来模拟这个过程,用一个 t o t tot tot记录当前有多少个不合法的偏序对,如果修改成功后 t o t = = 0 tot==0 tot==0,那么就是 Y E S YES YES,否则就是 N O NO NO

为了优化时间复杂度,采用 b i t s e t bitset bitset来存储二进制数,复杂度为 O ( Q ⋅ N 64 ⋅ log ⁡ N ) O\left( Q\cdot \frac{N}{64} \cdot \log N\right) O(Q64NlogN)

代码

#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define int ll
#define rep(i,a,b) for(int i=(a);i<=(b);i++)
#define per(i,a,b) for(int i=(a);i>=(b);i--)
 
const int N = 25005;
int n, q;
bitset<N>bs[N];
int cnt[N];
 
 
bool pd(int a, int b) {
    if (!a || !b)return 0;
    return (bs[a] & ~bs[b]).any();
}
 
void solve() {
    cin >> n >> q;
    set<pair<int, int>>s;//cnt,id
    rep(i, 1, n)s.insert({ 0,i }), bs[i] = 0, cnt[i] = 0;
    int tot = 0;
    rep(i, 1, q) {
        int r, c;cin >> r >> c;
        auto it = s.lower_bound({ cnt[r],r });
        int pre = 0, nxt = 0;
        if (it != s.begin())pre = prev(it)->second;
        if (next(it) != s.end())nxt = next(it)->second;
        if (pre && pd(pre, r))tot--;
        if (nxt && pd(r, nxt))tot--;
        if (pre && nxt && pd(pre, nxt))tot++;
        s.erase(it);
 
        bs[r][c] = 1;
        cnt[r]++;
 
        s.insert({ cnt[r],r });
        it = s.lower_bound({ cnt[r],r });
        pre = 0, nxt = 0;
        if (it != s.begin())pre = prev(it)->second;
        if (next(it) != s.end())nxt = next(it)->second;
        if (pre && pd(pre, r))tot++;
        if (nxt && pd(r, nxt))tot++;
        if (pre && nxt && pd(pre, nxt))tot--;
 
        if (!tot)cout << "YES\n";
        else cout << "NO\n";
    }
}
 
signed main() {
    ios::sync_with_stdio(0);
    cin.tie(0), cout.tie(0);
    int t = 1;
    cin >> t;
    while (t--)solve();
}

G2. 单调黑白矩阵 (困难版)

#数学

这是该问题的困难版本。两个版本的区别在于,在这个版本中, n n n q q q 的限制非常大。只有当你解决了该问题的所有版本后,才能进行 hack。

一个大小为 n × n n \times n n×n 的黑白矩阵是指一个具有 n n n 行和 n n n 列的矩阵,其中每个单元格都被涂成了黑色或白色。令黑白矩阵 C C C 中单元格 ( r , c ) (r, c) (r,c) 的颜色表示为 C [ r , c ] C[r, c] C[r,c]

如果一个矩阵 C C C 满足以下条件,我们就称其为单调的:

不存在两行 1 ≤ i < j ≤ n 1 \le i < j \le n 1i<jn 和两列 1 ≤ k < l ≤ n 1 \le k < l \le n 1k<ln 满足以下三个条件:

  1. C [ i , k ] = C [ j , l ] C[i, k] = C[j, l] C[i,k]=C[j,l]
  2. C [ j , k ] = C [ i , l ] C[j, k] = C[i, l] C[j,k]=C[i,l]
  3. C [ i , k ] ≠ C [ j , k ] C[i, k] \neq C[j, k] C[i,k]=C[j,k]

现有一个大小为 n × n n \times n n×n 的黑白矩阵 M M M,最初所有单元格均为白色。请处理 q q q 个如下类型的查询:

  • r   c r \ c r c:将矩阵 M M M 中单元格 ( r , c ) (r, c) (r,c) 的颜色修改为黑色。然后,判断 M M M 是否为单调矩阵。

对于每个查询,保证单元格 ( r , c ) (r, c) (r,c) 在查询前的颜色为白色。

请注意,更新是持久的。换句话说,一个查询中的颜色修改会影响后续的所有查询。

思路

本题思路是询问 G e m i n i Gemini Gemini得来,其中有许多组合数学的专业知识

在 Hard 版本中, N , Q N, Q N,Q 达到了 2 ⋅ 10 6 2 \cdot 10^6 2106,简单版的 Bitset O ( Q N / 64 ) O(QN/64) O(QN/64) 已经彻底跑不动了,我们需要一个 O ( N + Q ) O(N+Q) O(N+Q) 的“降维打击”做法。

破题点:利用平方和作为“指纹”判定单调性
这里涉及到一个组合数学的硬核结论:Gale-Ryser 定理

  1. 理论值:共轭分拆 (Conjugate Partition)
    设行黑格数序列为 R R R。我们定义一个理论上的列分布 R ∗ R^* R R k ∗ R^*_k Rk 表示“黑格子数量 ≥ k \ge k k 的行数”。
    • 定理推论:矩阵单调    ⟺    \iff 实际的列黑格数序列 C C C 是理论序列 R ∗ R^* R 的一个排列。
  2. 指纹识别:主控性与平方和
    在数学上,列分布 C C C 的“集中度”永远不会超过 R ∗ R^* R
    • 只要列分布的平方和 ∑ C i 2 \sum C_i^2 Ci2 达到了理论上限 ∑ ( R i ∗ ) 2 \sum (R^*_i)^2 (Ri)2,那么 C C C 就一定是 R ∗ R^* R 的一个排列,矩阵也就一定是单调的。

具体维护:
我们只需要实时维护两个“指纹”:

  • sumC:实际每一列黑格数的平方和 ∑ ( c n t C j ) 2 \sum (cntC_j)^2 (cntCj)2
  • sumR:理论共轭序列的平方和 ∑ ( R k ∗ ) 2 \sum (R^*_k)^2 (Rk)2

利用平方差公式 ( x + 1 ) 2 − x 2 = 2 x + 1 (x+1)^2 - x^2 = 2x+1 (x+1)2x2=2x+1,我们可以在 O ( 1 ) O(1) O(1) 时间更新:

  • 涂黑 ( r , c ) (r, c) (r,c) 时:
    • sumC 增加 2 × c n t C [ c ] + 1 2 \times cntC[c] + 1 2×cntC[c]+1
    • sumR 增加 2 × s u f R [ c n t R [ r ] + 1 ] + 1 2 \times sufR[cntR[r] + 1] + 1 2×sufR[cntR[r]+1]+1。(其中 s u f R [ k ] sufR[k] sufR[k] R k ∗ R^*_k Rk,表示当前黑格数 ≥ k \ge k k 的行数)。

只要 sumC == sumR,答案就是 YES

代码

#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define int ll
#define rep(i,a,b) for(int i=(a);i<=(b);i++)
#define per(i,a,b) for(int i=(a);i>=(b);i--)
 
const int N = 2e6 + 5;
int n, q, cntC[N], cntR[N], sufR[N];
int sumC, sumR;
 
 
void solve() {
    cin >> n >> q;
    rep(i, 1, n)cntC[i] = cntR[i] = sufR[i] = 0;
    sumC = sumR = 0;
    int tot = 0;
    rep(i, 1, q) {
        int r, c;cin >> r >> c;
        sumC += 2 * cntC[c] + 1;
        cntC[c]++;
        sumR += 2 * sufR[cntR[r] + 1] + 1;
        sufR[cntR[r] + 1]++;
        cntR[r]++;
        if (sumC == sumR)cout << "YES\n";
        else cout << "NO\n";
    }
}
 
signed main() {
    ios::sync_with_stdio(0);
    cin.tie(0), cout.tie(0);
    int t = 1;
    cin >> t;
    while (t--)solve();
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值