1103Div3-①博弈②DP③哈希枚举

3402 字
17 分钟
1103Div3-①博弈②DP③哈希枚举

1103Div3-博弈#

Dashboard - Codeforces Round 1103 (Div. 3) - Codeforces

更好的阅读体验

A-数学#

题意:

对数组a中的每个元素增加b中对应的数值,目标是使a中每个元素都相等;

限制:b中的数值范围需满足[1,k],找到最小的k

题解:

由于至少需要+1,所以最小的k即为max(a) + 1 - min(a)即可

c++
#include <bits/stdc++.h>
using namespace std;
int main() {
int t;
cin >> t;
while (t--) {
int n;
cin >> n;
vector<int> h(n);
for (int i = 0; i < n; i++) {
cin >> h[i];
}
int mx = *max_element(h.begin(), h.end());
int mn = *min_element(h.begin(), h.end());
cout << mx + 1 - mn << "\n";
}
return 0;
}

B-数学-余数#

题意:

给定一个长度为n的01字符串,和一个整数k,进行如下操作:

选择一个整数i,并反转位置i和i+k上的字符

题解:

将字符串下标按模k的余数分组,每次操作只会影响同一个余数类中的两个字符,且反转后每个余数类中1的个数的奇偶性不会变换,因而每个余数类中1的个数必须为偶数

c++
#include<bits/stdc++.h>
using namespace std;
int main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
int t;
cin >> t;
while(t--){
int n,k;
cin >> n >> k;
string s;
cin >> s;
vector<int> cnt(k,0);
for(int i = 0;i < n;i++){
if(s[i] == '1'){
cnt[i%k]++;
}
}
bool ok = true;
for(int x:cnt){
if(x&1){
ok = false;
break;
}
}
cout << (ok ? "YES" : "NO") << endl;
}
}

C-贪心+数学#

题意:

给定三个整数a,b,x

目标是使a==b

可以执行一下两种操作:

1.选择a,b中的一个并将其加上1

2.选择a,b中的一个便将其除以x并向下取值

题解:

把每个数所有可能的“连续除以x后得到的状态”全部列出来,最优策略一定是“若干次除法 + 最后若干次加法”,枚举两个数除法后的状态(v1,v2),代价为:除法次数1​+除法次数2​+∣v1​−v2​∣

c++
#include <iostream>
#include <vector>
#include <utility>
#include <cstdlib>
#include <algorithm>
using namespace std;
int main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
int t;
cin >> t;
while(t--){
long long a,b,x;
cin >> a >> b >> x;
vector<pair<long long,int>>va,vb;
long long cur = a;
int step = 0;
va.push_back({cur,step});
while(cur > 0){
cur /= x;
step++;
va.push_back({cur,step});
}
cur = b;
step = 0;
vb.push_back({cur,step});
while(cur > 0){
cur /= x;
step++;
vb.push_back({cur,step});
}
long long ans = llabs(a - b);
for(auto & [v1,s1] : va){
for(auto & [v2,s2] : vb){
ans = min(ans,(long long)s1 + s2 + llabs(v1- v2));
}
}
cout << ans << endl;
}
return 0;
}

D-博弈#

题目重述

给定一个数组 aa 和整数 kk

游戏规则如下:

  • 第一次可以删除任意元素;
  • 设上一次删除的元素值为 xx
  • 下一次只能删除满足
0yxk0 \le y-x \le k

的元素。

即后续删除的数字必须:

  • 不小于上一次删除的数字;
  • 与上一次删除数字的差值不超过 kk

无法进行操作的玩家输掉游戏。


正常情况下 Dabir 先手。

现在 Arseniy 可以代替 Dabir 执行第一步。

之后按照:

Egor → Dabir → Egor → Dabir ...

继续进行。

问是否存在一种第一步,使得无论 Dabir 如何操作,Egor 都能够保证获胜。


题解思路

由于后续只能选择不小于当前值的数字,因此游戏过程中的数字一定是单调不下降的。

对于同一个数值 vv

v v v v v ...

这些元素之间永远可以互相选择。

因此同一个数值实际上形成一个独立的“堆”。

设其出现次数为:

cnt[v]cnt[v]

情况一:存在偶数次出现的数字

假设某个数字出现了偶数次:

v v v v

Arseniy 第一手删除一个 vv

剩余:

v v v

数量变成奇数。

之后无论双方如何选择,只要游戏进入这个数值,

这些相同数字会被轮流取走:

Egor
Dabir
Egor
...

因为数量为奇数,

最后一个 vv 一定由 Egor 取走。

因此:

如果存在某个数字出现次数为偶数,则答案一定为 YES。


情况二:所有数字出现次数均为奇数

此时无法利用上一种方法。

设所有不同数字升序排列:

b1<b2<<bmb_1<b_2<\cdots<b_m

如果存在一对相邻不同数字满足:

bi+1bikb_{i+1}-b_i\le k

那么 Arseniy 可以先删除一个 bib_i

随后 Egor 立即选择:

bi+1b_{i+1}

由于两个数字之间可以相互连接,

Egor 能够抢占主动权并获得必胜局面。

因此:

如果存在相邻不同数字差值不超过 kk,答案仍然是 YES。


情况三:所有频率均为奇数,且相邻数字差值均大于 k

即:

bi+1bi>kb_{i+1}-b_i>k

对于所有 ii 都成立。

此时:

  • 不同数字之间完全无法转移;
  • 每个数字形成独立块;
  • 每个块大小均为奇数。

Arseniy 删除任意元素后:

  • 该块大小变为偶数;
  • 其余块仍为奇数。

此时 Dabir 总能通过最优策略获得最后一步。

因此:

不存在让 Egor 必胜的第一步。

答案为 NO。


判定条件

统计每个数字出现次数。

若存在:

cnt[v]0(mod2)cnt[v]\equiv0\pmod2

则直接输出:

YES

否则将所有不同数字排序。

若存在:

bi+1bikb_{i+1}-b_i\le k

则输出:

YES

否则输出:

NO

算法流程

  1. 使用哈希表统计每个数字出现次数;
  2. 若存在偶数频率,输出 YES;
  3. 对所有不同数字排序;
  4. 检查是否存在相邻数字差值不超过 k;
  5. 存在则 YES,否则 NO。

时间复杂度

统计频率:

O(n)O(n)

排序不同数字:

O(mlogm)O(m\log m)

其中 mm 为不同数字个数,且:

mnm\le n

总复杂度:

O(nlogn)O(n\log n)

满足题目要求。


py
from collections import Counter
for _ in range(int(input())):
n, k = map(int, input().split())
a = list(map(int, input().split()))
cnt = Counter(a)
ok = False
for c in cnt.values():
if c % 2 == 0:
ok = True
break
if ok:
print("YES")
continue
vals = sorted(cnt.keys())
for i in range(len(vals) - 1):
if vals[i + 1] - vals[i] <= k:
ok = True
break
print("YES" if ok else "NO")
c++
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int T;
cin >> T;
while (T--) {
int n, k;
cin >> n >> k;
map<int,int> cnt;
for (int i = 0; i < n; i++) {
int x;
cin >> x;
cnt[x]++;
}
bool ok = false;
for (auto &p : cnt) {
if (p.second % 2 == 0) {
ok = true;
break;
}
}
if (ok) {
cout << "YES\n";
continue;
}
vector<int> vals;
for (auto &p : cnt) {
vals.push_back(p.first);
}
for (int i = 0; i + 1 < (int)vals.size(); i++) {
if (vals[i + 1] - vals[i] <= k) {
ok = true;
break;
}
}
cout << (ok ? "YES" : "NO") << '\n';
}
return 0;
}

D-DP#

状态转换

频率数组

题目保证:

1ain1\le a_i\le n

因此统计每个数字出现次数:

freq[x]

例如:

1 1 2 2 2 4

对应:

freq[1]=2
freq[2]=3
freq[4]=1

DP定义

定义:

dp[i]

表示:

当前游戏来到数字 i 这一层时, 当前操作者是否处于必胜态。

其中:

dp[i]=1

表示必胜态。

dp[i]=0

表示必败态。


为什么只考虑数字大小

由于后续操作满足:

0yxk0\le y-x\le k

因此数字只能越来越大。

游戏过程一定是:

小数字 → 大数字 → 更大的数字

不会回头。

所以可以从大到小进行 DP。


连通块思想

设当前处理数字 i。

若最近出现过的数字为:

last

并且:

lasti>klast-i>k

说明:

i 无法转移到 last

两者属于不同连通块。

此时:

last=-1

重新开始计算一个新的独立游戏块。

代码:

if last != -1 and last - i > k:
last = -1

一个新块的情况

如果:

last == -1

说明当前数字是一个新块的最大数字。

此时只有:

freq[i]

个相同数字。

游戏退化成:

i i i i ...

双方只能轮流拿。

因此:

  • 奇数个 → 先手赢
  • 偶数个 → 先手输

即:

dp[i] = freq[i] % 2

代码:

dp[i] = v % 2

状态转移

假设:

dp[i+1]

已经求出。

当前数字出现:

v = freq[i]

次。


情况1:v为奇数

若:

v % 2 == 1

则当前玩家最终会把操作权交给对手。

因此状态发生翻转。

如果后继是必败态:

dp[i+1] == 0

则当前一定能赢:

dp[i] = 1

代码:

if v % 2 == 1 and dp[i+1] == 0:
dp[i] = 1

情况2:v为偶数

若:

v % 2 == 0

双方在这一层拿完后,

操作权不会改变。

因此:

dp[i] = dp[i+1]

当后继是必败态时:

dp[i]=0

代码:

elif v % 2 == 0 and dp[i+1] == 0:
dp[i] = 0

情况3:后继是必胜态

若:

dp[i+1] == 1

说明已经存在一种策略使当前玩家获胜。

于是:

f = True
break

说明 Arseniy 已经找到了一个第一步。

代码:

elif dp[i+1] == 1:
f = True
break

为什么答案是 f

变量:

f

表示:

是否存在一个位置, 能让删除一个元素后, Egor 面对的是必胜局面。

一旦找到:

f=True

即可立即结束。

最后:

print("YES" if f else "NO")

算法流程

  1. 统计每个数字出现次数;
  2. 从大到小扫描数字;
  3. 按照差值是否超过 k 划分连通块;
  4. 维护 dp[i] 表示当前数字开始是否为必胜态;
  5. 一旦发现存在可行第一步则输出 YES;
  6. 否则输出 NO。

时间复杂度

统计频率:

O(n)O(n)

DP扫描:

O(n)O(n)

总复杂度:

O(n)O(n)

空间复杂度:

O(n)O(n)

满足:

n2×105\sum n \le 2\times10^5

的要求。


dp
import sys
input = lambda: sys.stdin.readline().strip()
for _ in range(int(input())):
n, k = map(int, input().split())
nums = list(map(int, input().split()))
freq = [0] * (n + 1)
for v in nums:
freq[v] += 1
dp = [0] * (n + 2)
last = -1
f = False
for i in range(n, -1, -1):
v = freq[i]
if v == 0:
dp[i] = dp[i + 1]
continue
if last != -1 and last - i > k:
last = -1
if last == -1:
last = i
dp[i] = v % 2
if dp[i] == 0:
f = True
break
else:
last = i
if v % 2 == 1 and dp[i + 1] == 0:
dp[i] = 1
elif v % 2 == 0 and dp[i + 1] == 0:
dp[i] = 0
elif dp[i + 1] == 1:
f = True
break
print("YES" if f else "NO")

E-枚举#

题目重述

给定一个长度为 nn 的数组 aa

需要从中选出:

  • 两个 不重叠
  • 长度相同
  • 的子段

记长度为 LL

要求两个子段都满足:

可以重排成一个数列,使得相邻差为 1

并且拼接后整体仍满足该性质。


1. “好数组”的本质

一个数组可以重排为:

bibi1=1b_i - b_{i-1} = 1

等价于:

子段内所有不同值必须构成一个连续整数区间

即:

maxmin=distinct1\max - \min = \text{distinct} - 1

2. 子段合法性条件

对于一个区间 [i,j][i, j]

记:

  • min=min(aiaj)\min = \min(a_i \dots a_j)
  • max=max(aiaj)\max = \max(a_i \dots a_j)
  • len=ji+1len = j - i + 1

则:

合法    maxmin=len1\text{合法} \iff \max - \min = len - 1

并且必须保证:

所有数互不重复(等价于 distinct = len)


我们需要找到:

两个不重叠的合法区间,长度相同且尽可能大


转化为“窗口枚举问题”

Step 1:枚举所有合法区间

固定左端点 i,向右扩展 j:

维护:

  • 当前最小值 minval
  • 当前最大值 maxval
  • 用 visit 数组判重

只要满足:

maxval - minval == j - i

则说明当前区间是一个“连续排列区间”。


Step 2:记录每个长度的区间位置

我们维护两个数组:

minimum[x][len] = 最小右端点
maximum[x][len] = 最大左端点

含义:

对于值域起点 x:

  • 存所有长度为 len 的合法区间
  • 记录最左 / 最右覆盖情况

Step 3:本质优化

我们不直接存区间,而是:

记录“值域区间 + 长度”对应的最极端位置

即:

  • 最靠左的起点
  • 最靠右的终点

用于快速判断是否能选出两个不重叠区间。


Step 4:判断是否能选两个区间

枚举长度 L:

对于所有值域起点 x:

检查是否存在:

区间1:[x, x+L-1]
区间2:[x+L, ...]

并且:

  • 两个区间都存在合法子段
  • 且不重叠

判断条件:

minimum[x][L] < maximum[x+L][L]
or
minimum[x+L][L] < maximum[x][L]

1. 一个长度 L 的“好数组”

对应的是一个:

值域长度 = L 的连续排列段


2. 两个数组拼接仍合法

说明:

两个值域区间必须“无冲突”,即:

  • 不能交错
  • 必须能够拼成更大连续块

因此只需检查:

两个长度 L 的连续块是否能在数组中找到“互不重叠实例”


Step 1:枚举所有子段

双指针 + hash 判重:

  • min/max
  • 是否重复
  • 是否满足 max-min=len-1

Step 2:记录区间信息

更新:

minimum[minval][len] = min(minimum[minval][len], j)
maximum[minval][len] = max(maximum[minval][len], i)

Step 3:枚举答案

从大到小枚举 L:

对于每个起点 x:

检查是否存在两个合法区间且不重叠


复杂度分析

枚举子段:

最坏:

O(n2)O(n^2)

判重优化:

利用 s[val] = i 防止重复扩展

平均接近:

O(n2/amortized)O(n^2 / \text{amortized})

总复杂度:

n6000n \le 6000 下可通过。


py
t = int(input())
for _ in range(t):
n = int(input())
a = list(map(int, input().split()))
max_len = n // 2 + 2
INF = 10**9
minimum = [[INF] * max_len for _ in range(n + 1)]
maximum = [[-1] * max_len for _ in range(n + 1)]
vis = [0] * (n + 5)
for i in range(n):
minv = a[i]
maxv = a[i]
tag = i + 1
for j in range(i, n):
v = a[j]
if vis[v] == tag:
break
vis[v] = tag
minv = min(minv, v)
maxv = max(maxv, v)
length = j - i + 1
if maxv - minv == length - 1 and length < max_len:
minimum[minv][length] = min(minimum[minv][length], j)
maximum[minv][length] = max(maximum[minv][length], i)
ans = 0
for L in range(n // 2, 0, -1):
ok = False
for x in range(1, n - 2 * L + 2):
if minimum[x][L] < maximum[x + L][L] or minimum[x + L][L] < maximum[x][L]:
ok = True
break
if ok:
ans = L
break
print(ans)

  1. 子段“好数组” ⇔ 值域连续(max-min = len-1)
  2. 预处理所有合法子段
  3. 记录“值域 + 长度”的最极端位置
  4. 枚举长度 L 判断是否能找到两个不重叠合法块

先找所有“值域连续的子段”,再用二维记录其左右边界,最后判断是否存在两个同长度且不重叠的合法块。


1103Div3-①博弈②DP③哈希枚举
http://blog.7a7a68.xyz/posts/1103div3-博弈/
作者
Waning
发布于
2026-06-13
许可协议
CC BY-NC-SA 4.0
Profile Image of the Author
Waning
愿你明日如绚丽之花.
公告
还活着.
音乐
封面

音乐

暂未播放

0:00 0:00
暂无歌词
分类
标签
站点统计
文章
56
分类
8
标签
20
总字数
125,693
运行时长
0
最后活动
0 天前

目录