101 的题解
在解题之前提交题解的代码会导致封禁。
作者:
概述
本题给出一个只含 、
、
的数组,要求把每个
替换成
或
,使数组的分数(两端为
、中间全为
的子数组的最大长度)达到最大。核心结论是:只需把「第一个
之前最靠左的那个
」与「最后一个
之后最靠右的那个
」改成
,其余
一律改成
,两次线性扫描即可完成。
分析
核心观察
数组的分数只由其中所有 的位置决定。把值为
的下标从小到大记为
,则当
时
即相邻两个 的距离的最大值;
时分数为
,
时分数为
。原因是:子数组「两端为
、内部全为
」与「两端是这个数组中相邻的两个
」是一一对应的——只要两个
之间没有别的
,把中间的元素全部取成
就得到合法子数组;反之合法子数组的两端之间不可能还有
。
思路
只有 的位置可以自由选择,因此把问题放宽成两个集合:记
为可以放
的位置(即
的下标),记
为固定是
的位置(即
的下标)。任何一种替换方案最终得到的
位置集合
都满足
,反过来任意这样的
都能由一种替换方案实现。
上界。 设某组方案里分数由相邻的两个
取得,则
,且开区间
内没有任何
,自然也没有
中的位置。反过来,任取一对满足
,且
(两端都可以放
);
- 开区间
内没有
中的位置(中间没有本来就固定的
)
的下标,都能构造出一组方案:只把 、
改成
,区间内部与外部的
全部改成
。此时
、
是相邻的两个
,分数至少为
。于是最优分数恰好等于「所有满足上述两条的区间
的最大长度」(单个位置
也是一种候选,长度为
;若
为空则分数为
)。
构造。 下面这组替换可以达到这个上界:
- 从左往右找到第一个不是
的位置
(即最靠左的可放
的位置)。若
,把它改成
;若它本来就是
,不动。
- 对称地,从右往左找到第一个不是
的位置
。若
,把它改成
。
- 其余的
一律改成
。
「第一个 之前最靠左的
」就是第一个非零位置
(当它不是
时),右侧同理,所以上面的说法与题解结论一致。
正确性说明。 记构造后 的位置集合为 \(S^\*\)。任取一个候选区间
,证明 \(S^\*\) 中存在距离不小于
的一对相邻
。
- 若数组中本来没有
:所有可放
的位置都是
,最外侧的两个候选位置就是
与
,候选区间的最大长度即
,而构造恰好把这两个位置置为
、中间全置
,分数正好取到这个值。
- 否则设
为第一个原来的
、
为最后一个原来的
。
- 若
:由
知
是
之前的一个
,故
。而
与
之间没有原来的
(
是第一个),中间的
也不会被构造置
(它们既不是最左的
,也不在最右的
之右),所以
与
在 \(S^\*\) 中相邻,分数至少为
。最后一步用了
:若
,则
是区间
内固定的
,与候选区间的条件矛盾。
- 若
且
:与上一条完全对称,
,且
与
在 \(S^\*\) 中相邻,分数至少为
。这里
用到了
:若
,则固定的
落在区间
内(因为
),与候选区间的条件矛盾。
- 若
且
:取
为不超过
的最后一个原来的
、
为不小于
的第一个原来的
(由
与
知二者存在)。区间
内没有固定的
,所以
与
是原来的
中相邻的两个;又
内的
都不会被构造置
(它们既不是最左的
,也不是最右的
),所以
、
在 \(S^\*\) 中相邻,分数至少为
。
- 若
单个位置的候选 也成立:只要
非空,构造后至少有一个
,分数至少为
。综上,构造得到的分数不小于任何候选区间的长度,即达到最优。
具体示例
- 第三组
:没有原来的
,最左、最右的可放
位置分别是第
位与第
位,把它们改成
、中间全改成
,得到
,分数为
。不存在比它们更靠外的可放
位置,因此最优分数就是
。
- 第六组
:最左的非零位置本身就是
(不动);最右的非零位置是第
位的
,改成
,得到
,分数为
(第
位到第
位)。
- 第八组
:两端置
、中间置
,得到
,分数为
;整个数组只有
个元素,分数不可能超过
,故这是最优解。
- 第九组
:最左的非零位置是第
位的
,改成
;最右的非零位置是第
位的
,不动;第
位的
改成
,得到
,分数为
(第
位到第
位)。样例给出的参考输出是
,分数同样是
——最优解不唯一,本题由 special judge 判定。
算法步骤
- 读入测试用例个数
t。 - 对每个测试用例,读入长度
n与数组a。 - 从左往右扫描
a:若当前位置是,把它改成
并结束扫描;若当前位置是
,直接结束扫描;若当前位置是
,继续看下一个位置。
- 从右往左扫描
a:规则与上一步相同。 - 输出
n个整数,其中仍等于的元素写成
(等价于输出
a[i]与的较大值)。
复杂度分析
- 时间:每个测试用例只做两次扫描,
;全部测试用例合计
,而
。
- 空间:
,用于存储数组
。
实现注意事项
- 两次扫描相互独立:每次扫描最多改动一个位置,谁先谁后、会不会被另一次扫描看到,都不影响结果。
- 扫描在遇到
时必须立即结束。若越过这个
继续把后面的
也改成
,会把原本连续的长区间切断,分数反而变小——输出仍然合法,但会因不是最优解被 special judge 判错。
- 输出中不能残留
:没有参与两次扫描的
必须在输出时写成
,否则 checker 会以「非
位置与原数组不符 /
位置必须为 0 或 1」为由判错。
- 边界:
且
时,左扫描把它改成
,右扫描遇到
直接结束,输出
,分数为
。
- 全部为
时没有任何位置可以放
,两次扫描都不改动元素,输出全
,分数为
。
- 输入总量为
个整数,直接使用
cin/cout即可,加上ios::sync_with_stdio(false)更稳妥;注意每个测试用例单独输出一行。
源代码
#include <iostream>
#include <vector>
int main() {
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int t;
std::cin >> t;
while (t--) {
int n;
std::cin >> n;
std::vector<int> a(n);
for (int i = 0; i < n; ++i) {
std::cin >> a[i];
}
// 左侧:第一个非零位置若是 -1,就置为 1
for (int i = 0; i < n; ++i) {
if (a[i] == -1) {
a[i] = 1;
}
if (a[i] == 1) {
break;
}
}
// 右侧:对称处理
for (int i = n - 1; i >= 0; --i) {
if (a[i] == -1) {
a[i] = 1;
}
if (a[i] == 1) {
break;
}
}
// 其余 -1 输出为 0
for (int i = 0; i < n; ++i) {
std::cout << (a[i] > 0 ? a[i] : 0) << (i + 1 == n ? '\n' : ' ');
}
}
return 0;
}
import sys
def main():
data = sys.stdin.buffer.read().split()
pos = 0
t = int(data[pos])
pos += 1
out = []
for _ in range(t):
n = int(data[pos])
pos += 1
a = [int(x) for x in data[pos:pos + n]]
pos += n
# 左侧:第一个非零位置若是 -1,就置为 1
for i in range(n):
if a[i] == -1:
a[i] = 1
if a[i] == 1:
break
# 右侧:对称处理
for i in range(n - 1, -1, -1):
if a[i] == -1:
a[i] = 1
if a[i] == 1:
break
# 其余 -1 输出为 0
out.append(" ".join(str(x if x > 0 else 0) for x in a))
sys.stdout.write("\n".join(out) + "\n")
main()
评论