[R69F] 数组相同
- 难度 普及+/提高
- 时限 1s
- 空限 512m
- 构造折半枚举
数据规模:,。
思路
先分析两种操作的实质。
对序列 的操作给相邻两个位置同时加上同一个 ,因此 “奇偶交替和”
保持不变;反过来, 个相邻加操作张成的空间恰好是“交替和为零”的全体序列(等价于相邻差分的逐步调整),所以 可以变成任意一个交替和等于 的序列。
对序列 的操作只是交换相邻元素,可以把 变成任意排列,且总和 不变。
于是问题等价于:是否存在一个 的排列,使它的交替和恰好等于 。奇数位置共有 个,若放在奇数位的数之和为 ,偶数位的数之和为 ,则该排列的交替和为:
要让它等于 ,即 ,解得:
所以只需判断能否从 中选出恰好 个数,使它们的和等于 。若 是奇数则直接无解。
剩下的问题是“选固定个数、固定总和”的子集和问题。,直接枚举组合最多有 种,不可行,需要用折半枚举:
- 把 分成前后两半,各至多 个元素;
- 枚举左半所有子集,把“选中的个数 、总和 ”按个数 分组存入哈希表;
- 枚举右半所有子集,对每个(个数 、总和 ),在左半的哈希表里查找是否存在个数为 、总和为 的子集。
存在这样的划分即可通过相邻交换排出 ,再用 的操作对齐两个序列,输出 Yes。
复杂度
时间 ,空间 (最坏 时各 )。
仓颉实现
import std.convert.*
import std.collection.*
import std.env.*
main() {
let reader = getStdIn()
let n = Int64.parse(reader.readln().getOrThrow())
let a = reader.readln().getOrThrow().split(" ", removeEmpty: true).map({ p: String => Int64.parse(p) })
let b = reader.readln().getOrThrow().split(" ", removeEmpty: true).map({ p: String => Int64.parse(p) })
var sumA: Int64 = 0
for (i in 0..n) {
if (i % 2 == 0) {
sumA += a[i]
} else {
sumA -= a[i]
}
}
var sumB: Int64 = 0
for (x in b) {
sumB += x
}
if ((sumA + sumB) % 2 != 0) {
println("No")
return
}
let x = (sumA + sumB) / 2
let t = (n + 1) / 2
let k = n / 2
let nn = n
// 枚举左半部分(下标 0..k-1)的所有子集,按选中个数存和
let sets = Array<HashSet<Int64>>(k + 1, { _ => HashSet<Int64>() })
for (mask in 0..(1 << k)) {
var cnt = 0
var sum: Int64 = 0
var m = mask
var idx = 0
while (m > 0) {
if (m % 2 == 1) {
sum += b[idx]
cnt += 1
}
idx += 1
m /= 2
}
sets[cnt].add(sum)
}
// 枚举右半部分(下标 k..n-1)的所有子集,查左半能否补齐
let r = nn - k
for (mask in 0..(1 << r)) {
var cnt = 0
var sum: Int64 = 0
var m = mask
var idx = k
while (m > 0) {
if (m % 2 == 1) {
sum += b[idx]
cnt += 1
}
idx += 1
m /= 2
}
let c1 = t - cnt
if (c1 >= 0 && c1 <= k && sets[c1].contains(x - sum)) {
println("Yes")
return
}
}
println("No")
}
要点:
- 左半子集按选中个数分组存进
HashSet,右半枚举时以 的contains完成合并查询,避免排序后二分带来的额外 因子; - 子集用掩码枚举,逐位取出元素累加个数与总和,单次枚举 ;
- 和的最大绝对值不超过 ,
Int64足够,无需取模。