CF2053-Solution
【思维场!】CF2053解析
今天带来的是 CF2053:Good Bye 2024: 2025 is NEAR 前六题的解析,每道题配备了图片留白,你能坚持到第几关呢?
时间限制均为 2s ,内存限制均为 512 MB 。
A. Tender Carpenter
【题目描述】
定义一个集合
给出一个数组
询问是否存在两种不同的分割方式。(YES or NO)
【数据规模与约定】
【图片留白】
【个人解析】
第一种方式:每个元素单独一段,一共
第二种方式:在
附上个人 AC 代码:
#include <bits/stdc++.h>
using namespace std;
void solve ()
{
int n;
cin >> n;
vector<int> a(n, 0);
for (int i = 0; i < n; i++) cin >> a[i];
int f = 0;
for (int i = 0; i + 1 < n; i++)
{
int u = max(a[i], a[i + 1]);
int v = min(a[i], a[i + 1]);
if (v * 2 > u) f = 1;
}
if (f) cout << "YES\n";
else cout << "NO\n";
}
int main ()
{
int _;
cin >> _;
while (_--) solve();
return 0;
}
B. Outstanding Impressionist
【题目描述】
对于一个长度为
称下标
请你判断每个
【数据规模与约定】
【图片留白】
【个人解析】
记每个下标
下标
-
:只有一个选择。 -
:有多个选择。
需要先想到,如果所有下标
- 对于当前的
,选中区间内一个点,那么对于剩下的 ,选择数减一。但是它们本来就有多个选择,所以一定有得选。
然后推广到一般情况,分别考虑两类下标的策略。
为了方便描述,我们记一个第一类下标
对于第二类下标
- 如果
区间内的每个点都被第一类下标占领了,那么无论 选哪个,一定会让某个第一类下标没得选,因此 不独立。 - 否则,
选择一个没有被任何第一类下标占领的点即可。所有第一类下标都有得选,剩下的只有第二类下标。
对于第一类下标
-
对于第二类下标,本来就有多个选择,不构成影响;
-
对于
的第一类下标,本来唯一的选择就和 不同,不构成影响。 -
对于
的第一类下标,选择数变为零。这种情况一旦发生,就说明 不是唯一的!
我们在代码中讨论清楚上面的情况即可。
可以用前缀和等技巧优化时间复杂度到
#include <bits/stdc++.h>
using namespace std;
void solve ()
{
int n;
cin >> n;
vector<pair<int, int> > a(n);
vector<int> b(2 * n + 1, 0);
for (int i = 0; i < n; i++)
cin >> a[i].first >> a[i].second;
map<int, int> mp;
for (int i = 0; i < n; i++)
{
if (a[i].first != a[i].second) continue;
mp[a[i].first]++;
}
for (int i = 1; i <= 2 * n; i++)
if (!mp[i]) b[i] = 1;
for (int i = 1; i <= 2 * n; i++)
b[i] += b[i - 1];
for (int i = 0; i < n; i++)
{
auto [l, r] = a[i];
if (l == r) {
if (mp[l] == 1) cout << "1";
else cout << "0";
continue;
}
if (b[r] - b[l - 1]) cout << "1";
else cout << "0";
}
cout << "\n";
}
int main ()
{
int _;
cin >> _;
while (_--) solve();
return 0;
}
C. Bewitching Stargazer
【题目描述】
天空中有
最初,观测到的星星位于
希望在它观测到的每个星段
-
首先,它将计算
. -
如果星段的长度(即
)是偶数,将把它分成两个同样长的星段 和 ,以便进一步观测 . -
否则,将把望远镜瞄准
星,幸运值增加 ;随后,如果,将继续观测星段
和 .
随着观察的进行,不会继续观察任何长度严格小于
【数据规模与约定】
【图片留白】
【个人解析】
void dfs(int l, int r)
{
int mid = (l + r) / 2;
if ((r - l + 1) % 2 == 0)
{
dfs(l, mid);
dfs(mid + 1, r);
}
else
{
ans += mid;
dfs(l, mid - 1);
dfs(mid + 1, r);
}
}
需要想到,每次往右边的递归,是没有必要的。
设当前区间
-
往左区间
递归,和往右区间 递归,幸运值的增加次数,是一样的,因为它们在数轴上的长度相等,每次取中点,分割,取中点,分割 ... 是完全相同的两个过程。 -
唯一的区别在于,往右区间,幸运值每次增加,要多加
。 -
因此,让程序往左区间递归计算,记录幸运值的增加总量
,和增加次数 。 -
右区间的整体答案可以被描述为
。 -
区间
的幸运值增加次数可以被描述为 。 -
记录下这个答案,返回上一层函数即可。
整体时间复杂度
具体看代码。
#include <bits/stdc++.h>
using namespace std;
int n, k;
pair<long long, long long> dfs(int l, int r)
// first: 答案
// second: 提取的数量
{
if (r - l + 1 < k) return {0, 0};
long long u = 0, v = 0;
int mid = (l + r) / 2;
if ((r - l + 1) % 2 == 0)
{
auto ans = dfs(l, mid);
u = ans.first;
v = ans.second;
u += ans.first + 1ll * mid * ans.second;
v = v * 2;
return {u, v};
}
else
{
u = u + mid;
v = v + 1;
auto ans = dfs(l, mid - 1);
u += ans.first;
v += ans.second;
u += ans.first + 1ll * mid * ans.second;
v += ans.second;
return {u, v};
}
}
void solve ()
{
cin >> n >> k;
cout << dfs(1, n).first << "\n";
}
int main ()
{
int _;
cin >> _;
while (_--) solve();
return 0;
}
D. Refined Product Optimality
【题目描述】
- 给定两个数组
和 ,它们都包含 个整数。 - Iris关心的是通过任意重新排列数组
后, 的最大值。注意,她只关心 的最大值,并不需要实际重新排列 。 - 将有
次修改。每次修改可以用两个整数 和 表示,其中 为 或 ( )。若 ,则表示 增加 ;若 ,则表示 增加 。 - Iris会向Chris询问
的最大值,共有 次:一次是在任何修改之前,然后是每次修改之后。 - 由于
可能非常大,Chris只需要计算 。
Chris很快解决了这个问题,但他太累了,睡着了。现在,除了感谢Chris,你的任务是编写程序,计算给定输入数据的答案。
【数据规模与约定】
保证
【图片留白】
【个人解析】
首先考虑如何解决不带修改的版本。
询问重新排列数组
这里需要猜到,对数组
证明:
-
设
,一定有: 。 -
讨论
和 这两个区间的相对位置就可以证明上面不等式的正确性。为了方便讨论,不妨设 。具体的:
-
如果
整体在 的左侧,即 ,那么 。 。不等式成立。
-
如果
和 构成交叉但不覆盖的关系,即 ,那么:后者显然大于前者。不等式成立。
-
如果
和 构成覆盖的关系,即 ,那么:后者显然大于前者。不等式成立。
-
因此得证。
现在我们的问题转化为:
- 每次单点修改时,如何快速的维护
?
如果是一般的修改,其实是没法维护的,因为要维护
需要注意到题目中,只有
额外维护两个 map
,我们记为 map_a
和 map_b
。Key 类型是 int
,Value 类型是 pair<int, int>
。
map_a[u]
存储的信息是:数组 从小到大排序后,u
这个值分布的区间左右端点。(都从小到大排序了,u
一定是连续出现的)map_b[u]
存储的信息类似,只不过是对数组 存的。
额外维护两个数组
思路的关键是:利用 map_a
定位到
下面讨论让
- 用
map_a
定位到 这个值在排序后的数组 中,分布的区间的右端点 ; 除掉 。这里我们计划修改 中 这个值最后一次出现位置。模数意义下的除法用逆元来做。- 让右端点减
。如果 这个值在数组 中出现的次数已经是 了,就删除map_a
中的这个 Key 。 . .- 如果现在
这个值在map_a
中,让它的左端点减一。 - 如果不存在,插入
这个键值对到map_a
中。
对于
#include <bits/stdc++.h>
using namespace std;
const int mod = 998244353;
long long qpow(long long a, long long b)
{
long long ans = 1, base = a;
while (b)
{
if (b & 1) ans = ans * base % mod;
base = base * base % mod;
b /= 2;
}
return ans;
}
void solve ()
{
int n, q;
cin >> n >> q;
vector<int> a(n, 0);
vector<int> b(n, 0);
for (int i = 0; i < n; i++) cin >> a[i];
for (int i = 0; i < n; i++) cin >> b[i];
vector<int> pa(n, 0), pb(n, 0);
for (int i = 0; i < n; i++)
pa[i] = a[i], pb[i] = b[i];
sort(pa.begin(), pa.end());
sort(pb.begin(), pb.end());
long long ans = 1;
for (int i = 0; i < n; i++)
{
ans = ans * min(pa[i], pb[i]) % mod;
}
map<int, pair<int, int> > mpa, mpb;
for (int i = 0; i < n; i++)
{
int j;
for (j = i; j < n; j++)
{
if (pa[j] != pa[i]) break;
}
mpa[pa[i]] = {i, j - 1};
i = j - 1;
}
for (int i = 0; i < n; i++)
{
int j;
for (j = i; j < n; j++)
{
if (pb[j] != pb[i]) break;
}
mpb[pb[i]] = {i, j - 1};
i = j - 1;
}
cout << ans << " ";
while (q--)
{
int p, x;
cin >> p >> x;
x--;
if (p == 1)
{
auto [l, r] = mpa[a[x]];
ans = ans * qpow(min(pa[r], pb[r]), mod - 2) % mod;
r = r - 1;
mpa[a[x]].second--;
if (r < l) mpa.erase(a[x]);
a[x]++;
pa[r + 1]++;
if (mpa.count(a[x]))
{
mpa[a[x]].first--;
ans = ans * min(pa[r + 1], pb[r + 1]) % mod;
}
else
{
mpa[a[x]] = {r + 1, r + 1};
ans = ans * min(pa[r + 1], pb[r + 1]) % mod;
}
}
else
{
auto [l, r] = mpb[b[x]];
ans = ans * qpow(min(pa[r], pb[r]), mod - 2) % mod;
r = r - 1;
mpb[b[x]].second--;
if (r < l) mpb.erase(b[x]);
b[x]++;
pb[r + 1]++;
if (mpb.count(b[x]))
{
mpb[b[x]].first--;
ans = ans * min(pa[r + 1], pb[r + 1]) % mod;
}
else
{
mpb[b[x]] = {r + 1, r + 1};
ans = ans * min(pa[r + 1], pb[r + 1]) % mod;
}
}
cout << ans << " ";
}
cout << "\n";
}
int main ()
{
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int _;
cin >> _;
while (_--) solve();
return 0;
}
E. Resourceful Caterpillar Sequence
【题目描述】
有一棵由
诺拉和阿伦轮流移动毛毛虫,诺拉先移动。两位玩家都将使用各自的最优策略:
- 他们会让自己赢;
- 但是,如果不可能,他们就会努力阻止对方获胜(因此,游戏将以平局结束)。
轮到诺拉时,她必须选择一个与顶点
每当
请计算
【数据规模与约定】
【图片留白】
【个人解析】
把树上的点分为三类:
- A类点:本身是叶子。
- B类点:本身不是叶子,但和叶子相邻。
- C类点:既不是 A 类,也不是 B 类。
容易发现 ABC 这三类点是没有交集的。
阿伦获胜的方案也可以分为下面两类来统计:
-
是 A 类点, 是 BC 类点。这里说人话就是 是叶子, 不是叶子,算下叶子的数量就可以算出方案数。 -
是 C 类点,但是 往 路径以外的任何一个点拉动毛毛虫, 都会被拉到 B 类点。那已知 拉动一步后没到叶子, 被拉到了 B 类点, 操作一次就可以赢。这里可以枚举
,然后在此基础上枚举和 相邻的点 ,如果 是 B 类点,那么以 为根,子树 中的 C 类点都是合法的 ,统计数量即可。具体维护手法见代码。
#include <bits/stdc++.h>
using namespace std;
const int N = 2e5 + 5;
vector<int> g[N];
int sz[N], fa[N], b[N];
void dfs (int u)
{
if (g[u].size() != 1 && !b[u]) sz[u] = 1;
for (int v : g[u])
{
if (v == fa[u]) continue;
fa[v] = u;
dfs(v);
sz[u] += sz[v];
}
}
void solve ()
{
int n;
cin >> n;
for (int i = 1; i <= n; i++)
{
g[i].clear();
b[i] = 0;
sz[i] = 0;
}
for (int i = 1; i < n; i++)
{
int u, v;
cin >> u >> v;
g[u].push_back(v);
g[v].push_back(u);
}
int cnt = 0;
for (int i = 1; i <= n; i++)
{
if (g[i].size() == 1)
{
cnt++;
continue;
}
int f = 0;
for (int v : g[i])
{
if (g[v].size() == 1) f = 1;
}
if (f) b[i] = 1;
}
dfs(1);
long long ans = 1ll * cnt * (n - cnt);
int sum = 0;
for (int i = 1; i <= n; i++)
{
if (g[i].size() != 1 && !b[i]) sum++;
}
for (int i = 1; i <= n; i++)
{
if (g[i].size() == 1) continue;
for (int v : g[i])
{
if (b[v])
{
if (v != fa[i])
{
ans += sz[v];
}
else
{
ans += sum - sz[i];
}
}
}
}
cout << ans << "\n";
}
int main ()
{
int _;
cin >> _;
while (_--) solve();
}
F. Earnest Matrix Complement
【题目描述】
Aquawave 有一个大小为
您将填入
你必须找到填空后最美的
输出式子的最大值即可。
【数据规模与约定】
【图片留白】
【提示1】
- 如果保证每一行只有一个
-1
,你有什么填空的策略吗?
【图片留白】
【个人解析】
为了方便描述,记第 -1
的数量为
首先需要想到,一行中的所有 -1
都填相同的数,一定是这一行的其中一个解。
证明:
- 假设第
行和第 行都已经填完了,第 行是最后处理的,那么在第 行中的一个-1
处填数字 ,对答案的贡献是 ,和这个-1
的位置没有关系。 - 因此完全可以全部填同一个
,使得 最大化。
然后可以推广出一个
-
先忽略
-1
,计算出答案的一部分,也就是这一步可以
计算。 -
设计
数组, 表示仅考虑前 行,第 行不存在,在第 行填数字 的答案。 -
基于
的 个状态,如何计算 呢?有如下转移: -
上一行选的是
,其中 ,有 .其中,
这一部分,可以直接取所有 中的最大值,可以提前 计算好。因此这一步转移的复杂度是
的。 -
上一行选的也是
,有这一步转移也是
的。
所有的转移都是
可以写出如下代码:
#include <bits/stdc++.h>
using namespace std;
const int N = 2e5 + 5;
void solve ()
{
int n, m, k;
cin >> n >> m >> k;
int a[n + 1][m + 1] = {0};
long long dp[n + 1][k + 1] = {0};
long long c[n + 1] = {0};
long long d[n + 1][k + 1] = {0};
for (int i = 1; i <= n; i++) for (int j = 1; j <= m; j++) cin >> a[i][j];
for (int i = 1; i <= n; i++) for (int j = 1; j <= m; j++) if (a[i][j] == -1) c[i]++;
for (int i = 1; i <= n; i++) for (int j = 1; j <= m; j++) if (a[i][j] != -1) d[i][a[i][j]]++;
long long ans = 0;
for (int i = 2; i <= n; i++) for (int j = 1; j <= m; j++) if (a[i][j] != -1) ans += d[i - 1][a[i][j]];
for (int i = 2; i <= n; i++)
{
long long Max = 0;
for (int w = 1; w <= k; w++)
{
Max = max(Max, dp[i - 1][w] + c[i - 1] * d[i][w]);
}
for (int j = 1; j <= k; j++)
{
dp[i][j] = max(dp[i][j], Max + c[i] * d[i - 1][j]);
dp[i][j] = max(dp[i][j], dp[i - 1][j] + c[i] * c[i - 1] + c[i] * d[i - 1][j] + c[i - 1] * d[i][j]);
}
}
long long t = 0;
for (int i = 1; i <= k; i++) t = max(t, dp[n][i]);
cout << ans + t << "\n";
}
int main ()
{
int _;
cin >> _;
while (_--) solve();
}
现在设计
观察刚刚的 dp
转移过程,可以做出如下优化:
long long Max = 0;
for (int w = 1; w <= k; w++)
{
Max = max(Max, dp[i - 1][w] + c[i - 1] * d[i][w]);
}
对于这部分求解,可以在 for
循环外定义一个 Max
,用来存储
long long Max = 0;
for (int i = 2; i <= n; i++)
{
for (int j = 1; j <= m; j++)
{
int w = a[i][j];
if (w == -1) continue;
Max = max(Max, dp[w] + c[i - 1] * d[i][w]);
}
...// 记得计算当前这轮 dp 状态的时候更新 Max
dp[i][j] = max(dp[i][j], Max + c[i] * d[i - 1][j]);
dp[i][j] = max(dp[i][j], dp[i - 1][j] + c[i] * c[i - 1] + c[i] * d[i - 1][j] + c[i - 1] * d[i][j]);
对于这两部分转移,可以重新描述为:
- 先让所有
加上 ,作为
那么
和
-
如果
在上一行没有出现过,那等价于和 取较大值。这也可以用一个全局懒惰标记实现。
-
如果
在上一行出现过,那 暴力枚举这样的 ,更新 状态。
下面是我的 AC 代码,其中对懒惰标记、Max
变量的更新还有很多细节。
#include <bits/stdc++.h>
using namespace std;
const int N = 2e5 + 5;
void solve ()
{
int n, m, k;
cin >> n >> m >> k;
int a[n + 1][m + 1] = {0};
long long dp[k + 1] = {0};
long long c[n + 1] = {0};
map<int, int> d[n + 1];
for (int i = 1; i <= n; i++) for (int j = 1; j <= m; j++) cin >> a[i][j];
for (int i = 1; i <= n; i++) for (int j = 1; j <= m; j++) if (a[i][j] == -1) c[i]++;
for (int i = 1; i <= n; i++) for (int j = 1; j <= m; j++) if (a[i][j] != -1) d[i][a[i][j]]++;
long long ans = 0;
for (int i = 2; i <= n; i++) for (int j = 1; j <= m; j++) if (a[i][j] != -1 && d[i - 1].count(a[i][j])) ans += d[i - 1][a[i][j]];
long long lz = 0, lz1 = -1e18;
long long Max = 0;
for (int i = 2; i <= n; i++)
{
for (int j = 1; j <= m; j++)
{
int w = a[i][j];
if (w == -1) continue;
dp[w] = max(dp[w], lz1);
Max = max(Max, dp[w] + c[i - 1] * d[i][w]);
}
long long t = Max;
lz += c[i] * c[i - 1];
set<int> st;
for (int j = 1; j <= m; j++)
{
int w = a[i - 1][j];
if (w == -1) continue;
if (st.count(w)) continue;
dp[w] = max(dp[w], lz1);
dp[w] += c[i] * d[i - 1][w];
t = max(t, dp[w]);
st.insert(w);
}
st.clear();
for (int j = 1; j <= m; j++)
{
int w = a[i][j];
if (w == -1) continue;
if (st.count(w)) continue;
dp[w] += c[i - 1] * d[i][w];
t = max(t, dp[w]);
st.insert(w);
}
for (int j = 1; j <= m; j++)
{
int w = a[i - 1][j];
if (w == -1) continue;
dp[w] = max(dp[w], Max + c[i] * d[i - 1][w] - c[i] * c[i - 1]);
t = max(t, dp[w]);
}
lz1 = max(lz1, Max - c[i] * c[i - 1]);
Max = t;
}
long long t = 0;
for (int i = 1; i <= k; i++) t = max(t, dp[i] + lz);
cout << ans + t << "\n";
}
int main ()
{
int _;
cin >> _;
while (_--) solve();
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 零经验选手,Compose 一天开发一款小游戏!
· 因为Apifox不支持离线,我果断选择了Apipost!
· 通过 API 将Deepseek响应流式内容输出到前端