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)即可
#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的个数必须为偶数
#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∣
#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-博弈
题目重述
给定一个数组 和整数 。
游戏规则如下:
- 第一次可以删除任意元素;
- 设上一次删除的元素值为 ;
- 下一次只能删除满足
的元素。
即后续删除的数字必须:
- 不小于上一次删除的数字;
- 与上一次删除数字的差值不超过 。
无法进行操作的玩家输掉游戏。
正常情况下 Dabir 先手。
现在 Arseniy 可以代替 Dabir 执行第一步。
之后按照:
Egor → Dabir → Egor → Dabir ...继续进行。
问是否存在一种第一步,使得无论 Dabir 如何操作,Egor 都能够保证获胜。
题解思路
由于后续只能选择不小于当前值的数字,因此游戏过程中的数字一定是单调不下降的。
对于同一个数值 :
v v v v v ...这些元素之间永远可以互相选择。
因此同一个数值实际上形成一个独立的“堆”。
设其出现次数为:
情况一:存在偶数次出现的数字
假设某个数字出现了偶数次:
v v v vArseniy 第一手删除一个 。
剩余:
v v v数量变成奇数。
之后无论双方如何选择,只要游戏进入这个数值,
这些相同数字会被轮流取走:
EgorDabirEgor...因为数量为奇数,
最后一个 一定由 Egor 取走。
因此:
如果存在某个数字出现次数为偶数,则答案一定为 YES。
情况二:所有数字出现次数均为奇数
此时无法利用上一种方法。
设所有不同数字升序排列:
如果存在一对相邻不同数字满足:
那么 Arseniy 可以先删除一个 。
随后 Egor 立即选择:
由于两个数字之间可以相互连接,
Egor 能够抢占主动权并获得必胜局面。
因此:
如果存在相邻不同数字差值不超过 ,答案仍然是 YES。
情况三:所有频率均为奇数,且相邻数字差值均大于 k
即:
对于所有 都成立。
此时:
- 不同数字之间完全无法转移;
- 每个数字形成独立块;
- 每个块大小均为奇数。
Arseniy 删除任意元素后:
- 该块大小变为偶数;
- 其余块仍为奇数。
此时 Dabir 总能通过最优策略获得最后一步。
因此:
不存在让 Egor 必胜的第一步。
答案为 NO。
判定条件
统计每个数字出现次数。
若存在:
则直接输出:
YES否则将所有不同数字排序。
若存在:
则输出:
YES否则输出:
NO算法流程
- 使用哈希表统计每个数字出现次数;
- 若存在偶数频率,输出 YES;
- 对所有不同数字排序;
- 检查是否存在相邻数字差值不超过 k;
- 存在则 YES,否则 NO。
时间复杂度
统计频率:
排序不同数字:
其中 为不同数字个数,且:
总复杂度:
满足题目要求。
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")#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
状态转换
频率数组
题目保证:
因此统计每个数字出现次数:
freq[x]例如:
1 1 2 2 2 4对应:
freq[1]=2freq[2]=3freq[4]=1DP定义
定义:
dp[i]表示:
当前游戏来到数字 i 这一层时, 当前操作者是否处于必胜态。
其中:
dp[i]=1表示必胜态。
dp[i]=0表示必败态。
为什么只考虑数字大小
由于后续操作满足:
因此数字只能越来越大。
游戏过程一定是:
小数字 → 大数字 → 更大的数字不会回头。
所以可以从大到小进行 DP。
连通块思想
设当前处理数字 i。
若最近出现过的数字为:
last并且:
说明:
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 = Truebreak说明 Arseniy 已经找到了一个第一步。
代码:
elif dp[i+1] == 1: f = True break为什么答案是 f
变量:
f表示:
是否存在一个位置, 能让删除一个元素后, Egor 面对的是必胜局面。
一旦找到:
f=True即可立即结束。
最后:
print("YES" if f else "NO")算法流程
- 统计每个数字出现次数;
- 从大到小扫描数字;
- 按照差值是否超过
k划分连通块; - 维护
dp[i]表示当前数字开始是否为必胜态; - 一旦发现存在可行第一步则输出 YES;
- 否则输出 NO。
时间复杂度
统计频率:
DP扫描:
总复杂度:
空间复杂度:
满足:
的要求。
import sysinput = 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-枚举
题目重述
给定一个长度为 的数组 。
需要从中选出:
- 两个 不重叠
- 且 长度相同
- 的子段
记长度为 。
要求两个子段都满足:
可以重排成一个数列,使得相邻差为 1
并且拼接后整体仍满足该性质。
1. “好数组”的本质
一个数组可以重排为:
等价于:
子段内所有不同值必须构成一个连续整数区间
即:
2. 子段合法性条件
对于一个区间 :
记:
则:
并且必须保证:
所有数互不重复(等价于 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]orminimum[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:
检查是否存在两个合法区间且不重叠
复杂度分析
枚举子段:
最坏:
判重优化:
利用 s[val] = i 防止重复扩展
平均接近:
总复杂度:
在 下可通过。
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)- 子段“好数组” ⇔ 值域连续(max-min = len-1)
- 预处理所有合法子段
- 记录“值域 + 长度”的最极端位置
- 枚举长度 L 判断是否能找到两个不重叠合法块
先找所有“值域连续的子段”,再用二维记录其左右边界,最后判断是否存在两个同长度且不重叠的合法块。