【题解】Harbour.Space Scholarship Contest 2023-2024 D,E,F(CF1864)
D.Matrix Cascade
题目描述:
有一个大小为\(n \times n\)的矩阵,由 0 和 1 组成。行的编号从上到下依次为\(1\)到\(n\),列的编号从左到右依次为\(1\)到\(n\)。第\(x\)行与第\(y\)列交叉处的单元格记为\((x, y)\)。
水月想把矩阵的所有元素都变成 0。她可以一步完成以下操作:
- 选择任意一个单元格,假设它是\((i, j)\),然后反转\((i, j)\)中的元素,并反转\((x, y)\)单元格中的所有元素,要求 \(x > i\) 且 \(x-i \ge \left| y - j\right|\)。将一个值反转意味着将它变为相反的值:0 变为 1,1 变为 0。
帮助 AquaMoon 确定将矩阵中的所有元素变为 0 所需的最少步骤数。我们可以证明答案总是存在的。
\(2 \le n \le 3000\)
题目分析:
首先肯定是观察一次操作是在干什么:将一个顶点为 \((i,j)\) 的下三角形反转。
那么怎么反转最优呢,一个经典的思考方式就是矩阵第 \(1\) 行的 \(1\) 怎么反转,显然只有对 \((1,x)\) 进行一次操作才行。
这样我们就搞完了第一行,然后就考虑第二行,就从上到下依次考虑每一行,若当前位置为 \(1\) 则反转。
下面的问题就是如何维护当前位置的值是什么,如果每次操作都暴力做的话就是 \(O(n^3)\),而且好像也没什么可以直接维护的方式。
那么我们就考虑不直接做,而是对于每一个位置维护这个位置被反转了几次,可以发现有贡献的点就是对应着一个以 \((i,j)\) 为顶点的上三角形。
我们可以设 \(f_{i,j}\) 表示 \((i,j)\) 被反转的次数,则有 \(f_{i,j} = f_{i-1,j} + val1_{i+j} + val2_{i-j}\)。
这个转移具体来说就是考虑从上一行到这一行新增的有贡献的点是哪些,就是 \((i,j)\) 所在的两条对角线上的操作,而这两条对角线一条满足 \(i+j\) 为定值,另一条满足 \(i-j\) 为定值,所以可以直接根据这个统计。
而当我们要操作点 \((i,j)\) 时就是将 \(f_{i,j} + 1 \to f_{i,j},val1_{i+j} + 1 \to val1_{i+j},val2_{i-j} + 1 \to val2_{i-j}\) 即可。
代码:
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 3005;
int tmp[N][N];
char s[N][N];
map<int,int> mp1,mp2;
int main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
int T;scanf("%d",&T);
while(T--){
int n;scanf("%d",&n);
for(int i=1; i<=n; i++) scanf("%s",s[i]+1);
int ans = 0;
for(int i=1; i<=n; i++){
for(int j=1; j<=n; j++){
tmp[i][j] = tmp[i-1][j] + mp1[i+j] + mp2[i-j];
if((tmp[i][j] & 1) != ((s[i][j] - '0') & 1)){
++ans;
mp1[i+j]++,mp2[i-j]++;
tmp[i][j]++;
}
}
}
printf("%d\n",ans);
mp1.clear(),mp2.clear();
for(int i=1; i<=n; i++){
for(int j=1; j<=n; j++){
tmp[i][j] = 0;
}
}
}
return 0;
}
E.Guess Game
题目描述:
卡罗尔有一个由 \(n\)个非负整数组成的序列 \(s\)。她想与 Alice 和 Bob 玩 "猜谜游戏"。
为了玩这个游戏,卡罗尔将在范围 \([1, n]\)内随机选择两个整数指数 \(i_a\) 和 \(i_b\),并设置 \(a=s_{i_a}\), \(b=s_{i_b}\)。请注意,\(i_a\)和\(i_b\)可能重合。
卡罗尔会进行如下步骤:
- 将\(a\)的值告诉 Alice;
- 将\(b\)的值告诉 Bob;
- 同时告诉 Alice 和 Bob \(a \mid b\)的值,其中 \(|\)表示 bitwise OR 运算。
请注意,卡罗尔不会告诉 Alice 或 Bob 任何关于\(s\)的信息。
然后开始猜谜。两名玩家轮流猜测,Alice 先开始。双方的目标都是确定以下哪项为真 \(a < b\)、\(a > b\)或\(a = b\)。
在自己的回合中,玩家可以做以下两件事之一:
- 说 "我不知道",并将回合传给另一名玩家;
- 说 "我知道",然后回答"\(a<b\)"、"\(a>b\)"或"\(a=b\)";然后游戏结束。
Alice 和 Bob 能听到对方说的话,并能利用这些信息推断出答案。Alice 和 Bob 都很聪明,只有在完全确定时才会说 "我知道"。
你需要计算玩家在游戏中回合数的期望值。输出答案,取模 \(998\,244\,353\)。
形式上,让 \(M = 998\,244\,353\)。可以证明答案可以表示为不可约分数 \(\frac{p}{q}\),其中 \(p\)和 \(q\)是整数,而 \(q \not \equiv 0 \pmod{M}\)是不可约分数。输出等于 \(p \cdot q^{-1} \bmod M\)的整数。换句话说,输出这样一个整数 \(x\),即 \(0 \le x < M\) 和 \(x \cdot q \equiv p \pmod{M}\)。
题目分析:
首先期望显然可以转化为总代价除以总方案数。
总方案数是很好算的,就是 \(n \times n\),就是考虑总代价怎么计算。
要计算总代价必须要做到计算:给定 \(a,b\) 得到轮数是多少,其实就是要弄明白 "我不知道" 有什么作用。
首先我们可以仅保留 \(a | b\) 中为 \(1\) 的位,因为除了这些位会出现疑问其他的都没问题了。
如果 Alice 发现 \(a\) 的最高位为 \(0\) 那么就意味着 \(b\) 的最高位一定是 \(1\) 这个时候会立刻知道 \(a < b\)。
否则 Alice 就无法确定大小关系是什么,也就是此时的 "我不知道" 相当于说明了其最高位为 \(1\)。
也就是说第 \(i\) 次回答 "我不知道" 相当于对应的人说明了其知道的数的从高到低第 \(i\) 位为 \(1\),那么最终确定大小关系肯定就是到达了某个当前位为 \(0\) 的位置。
下面就分类讨论一下,将这个轮数转化为形式化的语言,下文中默认只保留 \(a | b\) 中为 \(1\) 的位:
- 若 \(a < b\),则设 \(a\) 中第一个 \(0\) 的位置为 \(i\),则答案为 \(i + 1 - [i\%2 = 1]\)
- 若 \(a=b\),则设其总共有 \(k\) 位,则答案为 \(k + 1\)
- 若 \(a > b\),则设 \(b\) 中第一个 \(0\) 的位置为 \(i\),则答案为 \(i + [i\%2 = 1]\)
这个时候统计总代价就可以考虑枚举 \(b\) 然后对于第一种贡献就是枚举 \(a\) 中第一个 \(0\) 的位置(也可以转化为枚举对应的 \(b\) 中的 \(1\) 的位置),第三种贡献就是枚举 \(b\) 中第一个 \(0\) 的位置,第二种贡献就是对于相等的特殊处理。
因为需要使用 map 所以复杂度是 \(O(n\log n \log V)\) 能过。
代码:
点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 5e5+5;
const int MOD = 998244353;
int a[N];
unordered_map<int,int> mp[32];
int power(int a ,int b){
a %= MOD;
int res = 1;
while(b){
if(b & 1) res = res * a %MOD;
a = a * a % MOD;
b >>= 1;
}
return res;
}
signed main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
int T;scanf("%lld",&T);
while(T--){
int n;scanf("%lld",&n);
for(int i=1; i<=n; i++) scanf("%lld",&a[i]);
sort(a+1,a+n+1);
int ans = 0;
//统计 a < b 的答案
int tot = 0;
for(int i=1; i<=n; i++){
tot = 0;
for(int j=30; j>=0; j--){
if((a[i] >> j) & 1){
++tot;
ans = (ans + mp[j][(a[i] >> j) ^ 1] * (tot + 1 - (tot % 2 == 1)))%MOD;
}
}
for(int j=30; j>=0; j--){
mp[j][a[i]>>j]++;
}
}
for(int i=0; i<=30; i++) mp[i].clear();
//统计 a > b 的答案
for(int i=n; i>=1; i--){
tot = 0;
for(int j=30; j>=0; j--){
if(!((a[i] >> j) & 1)){
ans = (ans + mp[j][(a[i] >> j) ^ 1] * (tot + 1 + ((tot + 1) % 2 == 1)))%MOD;
}
else ++tot;
}
for(int j=30; j>=0; j--){
mp[j][a[i]>>j]++;
}
}
for(int i=0; i<=30; i++) mp[i].clear();
//统计 a = b 的答案
for(int i=1; i<=n;){
int pos = i;
for(;pos <= n && a[pos] == a[i]; pos++);
--pos;
int tmp = __builtin_popcount(a[i]),len = pos - i + 1;
ans = (ans + len * len %MOD * (tmp + 1) %MOD)%MOD;
i = pos + 1;
}
printf("%lld\n",ans * power(n * n,MOD-2) %MOD);
}
return 0;
}
F.Exotic Queries
题目描述:
AquaMoon 给 RiverHamster 一个整数序列 \(a_1,a_2,\dots,a_n\),RiverHamster 给你\(q\)个查询。每个查询由两个整数\(l\)和\(r\)表示。
对于每个独立的查询,您可以取序列的任意连续段,然后从该段的所有数字中减去一个相同的非负值。您可以多次(可能是零次)这样做。但是,您不能选择两个互不包含的相交线段。您的目标是将初始值在 \([l, r]\)范围内的所有数字转换成 \(0\)。您必须用最少的运算次数完成这项工作。
请注意,查询是独立的,数组中的数字会在两次查询之间恢复到初始值。
题目分析:
因为每次都必须要求线段相交,所以一个显然的贪心策略就是每次将最小值减成 \(0\) 然后序列就被这些 \(0\) 分割成了一个个段,然后对于每一段递归处理即可。
我们很想让同一个数在一次操作内全部被搞掉,但是有的时候并做不到,那么是什么时候做不到这件事呢。
对于一个数 \(x\) 设其出现的位置为 \(p_1,p_2,\cdots,p_m\),则对于 \(p_i,p_{i+1}\) 若有一个数将它们分割开了它们就不能在一起处理了,也就是存在 \(y \in [p_i+1,p_{i+1}-1]\) 且满足 \(a_y < x\),因为我们存在值域在 \([l,r]\) 的限制,所以这里其实限制是 \(l \le a_y < x\)。
而每次满足这个限制也就意味着我们的答案要加 \(1\)。
显然最优的一个 \(a_y\) 就是 \([p_i+1,p_{i+1}-1]\) 的最大值,考虑对于由数 \(x\) 分割出来的每一段都预处理出这一段的最大值,设其构成集合 \(S_x\)。
那么因为我们一个查询可以等价于只保留值域在 \([l,r]\) 内的数,所以一个查询的答案就是 \(S_l,S_{l+1},\cdots,S_r\) 中大于等于 \(l\) 的数的个数(设前面这个的查询为 \(T(l,r,l)\))加不同种类的数的个数。
不同种类数的个数很好做,而对于 \(T(l,r,l)\) 显然可以拆成 \(T(1,r,l) - T(1,l-1,l)\),这样就只需要按值域从小到大扫描线每次加入 \(S_x\) 即可。
代码:
点击查看代码
#include <bits/stdc++.h>
using namespace std;
int n, q, l, r, mx, a[1000010], ans[1000010], s[1000010];
struct Query{
int x, id, op;
};
vector <Query> ask[1000010];
vector <int> pos[1000010];
struct Segment{
#define lc(x) x<<1
#define rc(x) x<<1|1
int d[4000010];
void pushup(int k){
d[k] = max(d[lc(k)], d[rc(k)]);
}
void modify(int k, int l, int r, int x, int y){
if (l == r){
d[k] = y;
return ;
}
int mid = l + r >> 1;
if (x <= mid) modify(lc(k), l, mid, x, y);
else modify(rc(k), mid+1, r, x, y);
pushup(k);
}
int query(int k, int l, int r, int x, int y){
if (x <= l && r <= y) return d[k];
int mid = l + r >> 1, ret = 0;
if (x <= mid) ret = max(ret, query(lc(k), l, mid, x, y));
if (y > mid) ret = max(ret, query(rc(k), mid+1, r, x, y));
return ret;
}
}S;
struct BIT{
int d[1000010];
void add(int p, int x){
for (; p<=n; p+=p&-p) d[p] += x;
}
int query(int p){
int ret = 0;
for (; p; p-=p&-p) ret += d[p];
return ret;
}
}T;
int main(){
scanf ("%d%d", &n, &q);
for (int i=1; i<=n; i++){
scanf ("%d", &a[i]);
pos[a[i]].push_back(i);
}
for (int i=1; i<=n; i++){
if (pos[i].size()) s[i] = s[i-1] + 1;
else s[i] = s[i-1];
}
for (int i=1; i<=q; i++){
scanf ("%d%d", &l, &r);
ans[i] += s[r] - s[l-1];
ask[l-1].push_back((Query){l, i, -1});
ask[r].push_back((Query){l, i, 1});
}
for (int i=1; i<=n; i++){
for (int j=0; j<pos[i].size(); j++){
if (j > 0){
int mx = S.query(1, 1, n, pos[i][j-1], pos[i][j]);
if (mx) T.add(mx, 1);
}
}
for (int j=0; j<pos[i].size(); j++){
S.modify(1, 1, n, pos[i][j], i);
}
for (int j=0; j<ask[i].size(); j++){
int x = ask[i][j].x, id = ask[i][j].id, op = ask[i][j].op;
ans[id] += op * (T.query(n) - T.query(x-1));
}
}
for (int i=1; i<=q; i++){
printf ("%d\n", ans[i]);
}
return 0;
}