题解 | #牛客小白月赛87 #
小苯的石子游戏
https://ac.nowcoder.com/acm/contest/73854/A
(注:题解中的代码只给出了核心逻辑部分)
A. 小苯的石子游戏
考点:贪心
题意:两个人玩游戏轮流从一个有序数组中取数字并加到自己的数字中,判断先手取的最终数字是否能严格大于后手。
由于数组已经有序,因此从后往前直接贪心取就行,每个人一定都取当前数组中最大的一个。
代码
void solve() {
int n;
cin >> n;
vector<int> a(n + 1);
for(int i = 1; i <= n; i++) {
cin >> a[i];
}
// sort(a.begin() + 1, a.end()); // 题目已经保证a有序,可以不写这句
int s1 = 0, s2 = 0;
int f = 0;
for(int i = n; i; i--) {
if(!f) s1 += a[i];
else s2 += a[i];
f ^= 1;
}
if(s1 > s2) {
cout << "Alice" << endl;
}
else {
cout << "Bob" << endl;
}
}
B. 小苯的排序异或
考点:贪心,排序
题意:给定一个数组,选择一段长度小于数组长度的区间进行排序,问能否操作一次后使得数组有序。
贪心考虑,区间长度要严格小于 ,恰好选 的长度一定是最优的。而 的区间长度只对应了 2 种情况:
和
因此分类讨论,只要两种情况有一种合法即可,也就是:
1. 到 的所有数都大于等于
2. 到 的所有数都小于等于
代码
void solve() {
int n;
cin >> n;
vector<int> a(n + 1);
int mn = 1e10, mx = 0;
for(int i = 1; i <= n; i++) {
cin >> a[i];
if(i > 1) mn = min(mn, a[i]);
if(i < n) mx = max(mx, a[i]);
}
if(mn >= a[1] || mx <= a[n]) {
cout << "YES" << endl;
}
else {
cout << "NO" << endl;
}
}
C, D. 小苯的IDE括号操作
考点:栈,双指针,模拟
题意:给定括号串,其中恰好出现一个 "I" 字符表示鼠标光标。处理 q 次操作,包括两种删除和两种移动(easy只有删除),删除和移动操作的规则和牛客在线IDE相同,求最终括号串的样子。
我们只需要用栈模拟即可,开两个栈分别正序存储光标左侧的所有字符,以及倒序存储光标右侧的所有字符。
对于 操作,左栈 一定 ,那只要左栈栈顶为 “(”,且同时右栈栈顶为 “)”,则右栈也需要 pop。
对于 操作,直接右栈 即可。
对于 两种移动 操作,我们只需要将一个栈的栈顶移动到另一个栈的栈顶,就相当于完成了移动操作。
注意以上处理所涉及的 操作均需判栈是否为空。
对于,我们还可以使用分别走向左右两端的双指针进行模拟,其过程和栈模拟都是类似的。
easy代码(双指针):
void solve() {
int n, q;
string s;
cin >> n >> q >> s;
int k = s.find('I');
int l = k - 1, r = k + 1;
while(q -- ) {
string t;
cin >> t;
if(t == "backspace") {
if(l >= 0) {
if(s[l] == '(' && r < n && s[r] == ')') r ++ ;
l -- ;
}
}
else {
if(r < n) r ++ ;
}
}
for(int i = 0; i <= l; i++) cout << s[i];
cout << 'I';
for(int i = r; i < n; i++) cout << s[i];
cout << endl;
}
hard 代码(栈):
void solve() {
string s;
int n, q;
cin >> n >> q >> s;
int k = s.find('I');
vector<char> a, b;
for(int i = 0; i < k; i++) {
a.emplace_back(s[i]);
}
for(int i = k + 1; i < n; i++) {
b.emplace_back(s[i]);
}
reverse(b.begin(), b.end());
while(q -- ) {
string t;
cin >> t;
if(t == "delete") {
if(b.size()) b.pop_back();
}
else if(t == "backspace") {
if(a.size()) {
if(a.back() == '(' && b.size() && b.back() == ')') {
b.pop_back();
}
a.pop_back();
}
}
else if(t == "<-") {
if(a.size()) {
b.emplace_back(a.back());
a.pop_back();
}
}
else {
if(b.size()) {
a.emplace_back(b.back());
b.pop_back();
}
}
}
for(int i = 0; i < a.size(); i++) {
cout << a[i];
}
cout << "I";
for(int i = b.size() - 1; ~i; i--) {
cout << b[i];
}
}
同时,本题也可以使用双向链表通过,但过程相对栈模拟复杂很多,以下给出某验题同学代码:
hard 代码(双向链表):
author:Kidulthood
int main() {
scanf("%d%d%s", &n, &k, s + 1);
for(int i = 1; i <= n; i ++) {
if(s[i] == '(') a[i] = -1;
else if(s[i] == ')') a[i] = 1;
else a[i] = 0, now = i;
}
for(int i = 1; i < n; i ++) R[i] = i + 1;
for(int i = 2; i <= n; i ++) L[i] = i - 1;
for(int i = 1; i <= k; i ++) {
scanf("%s", op + 1);
if(op[1] == 'b') {
if(a[L[now]] == -1 && a[R[now]] == 1) {
int l = L[L[now]], r = R[R[now]];
R[L[L[now]]] = now; L[R[R[now]]] = now;
L[now] = l; R[now] = r;
}
else {
if(L[now]) {
int l = L[L[now]];
R[l] = now; L[now] = l;
}
}
}
else if(op[1] == 'd') {
if(R[now]) {
int r = R[R[now]]; L[r] = now; R[now] = r;
}
}
else if(op[1] == '<') {
if(L[now]) swap(a[now], a[L[now]]), now = L[now];
}
else if(R[now]) swap(a[now], a[R[now]]), now = R[now];
}
while(L[now]) now = L[now];
while(now) {
if(a[now] == -1) printf("(");
else if(a[now] == 0) printf("I");
else printf(")");
now = R[now];
}
return 0;
}
E. 小苯的数组构造
考点:思维,构造,贪心
题意:给定长为 的一个数组 ,要求构造一个长为 的数组 ,满足 + 是单调不降的,且 的极差最小。
首先容易想到,如果没有极差的限制,那将非常容易构造,因为此时 数组可以尽可能地递增使 对其的影响忽略不计。因此容易再想到,答案具备单调性,很大的极差一定满足,那肯定存在一个最小的极差满足条件。
考虑到 的前缀最大值数组,只要将 变为 即 的前缀最值就是最优的。
我们将 的前缀最大值数组记为 ,则 的最大值就是本题的最小极差,即“每一项前面的最大值和该项的差值”的最大值,我们将其记为 。上述提到了答案具有单调性,我们只需证明 不能作为本题的答案即可,而这个证明十分显然,我们记 对应的前缀最大值的下标为 ,当前下标为 ,则有:。
此时如果我们设置 的极差为 ,则 的值最大只有 。
我们记 + ,则:
而这个式子在最优情况下,应该等于:,因此 不满足单调不降。
因此答案最少也得选择 。
代码
void solve() {
int n;
cin >> n;
vector<int> a(n + 1);
for(int i = 1; i <= n; i++) {
cin >> a[i];
}
int mx = a[1];
vector<int> ans(n + 1);
for(int i = 2; i <= n; i++) {
int now = max(0LL, mx - a[i]);
ans[i] = now;
mx = max(mx, a[i]);
}
for(int i = 1; i <= n; i++) {
cout << ans[i] << " \n"[i == n];
}
}
F.小苯的数组切分
考点:位运算,贪心
题意:给定长为 的数组 ,要求将数组分为非空的三段,段内分别做:异或,按位或,按位与。后结果相加求和,使结果最大,求最大和。
考虑位运算性质, 操作越做越大, 操作越做越小。因此我们直接取第三段区间为 这个点一定最优,接着枚举前两段区间的分割点即可。
分割点我们记为 和 ,为什么这样一定最优?
假设最终 不取在 的位置,如同本题的样例解释一样,那么我们容易发现,如果此时将 右移,第二段区间的值内部在做 运算,因此数字越多结果一定越大。相应的,第三段区间在做 运算,因此数字越少结果一定越大。
因此结果一定不会更差,所以我们直接取 一定最优,接着枚举 即可。
代码
void solve() {
int n;
cin >> n;
n -- ;
vector<int> a(n + 1), s(n + 1);
for(int i = 1; i <= n; i++) {
cin >> a[i];
s[i] = s[i - 1] ^ a[i];
}
int ans;
cin >> ans;
int p = a[n];
int sum = 0;
for(int i = n - 1; i; i--) {
sum = max(sum, p + s[i]);
p |= a[i];
}
cout << ans + sum << endl;
}
G.小苯的逆序对
考点:莫比乌斯反演,容斥,树状数组,dp
题意:给定长为 的排列,求有多少个排列值互素的逆序对。
我们可以定义 表示:“所有的 可以是 的倍数的数字们组成的数组”的逆序对数,假设我们已经有了这个 数组,那么我们如何将倍数的值转为其值本身,则只需要从大到小枚举 ,并执行:,执行了这一步后, 就转为了:“所有的 可以等于 的数”组成数组的逆序对个数。那么显然 就是我们要求的答案。
接下来考虑,我们如何获得一开始的 数组呢,也就是:“所有的 可以是 的倍数的数字们组成的数组”的逆序对数。我们只需枚举 ,然后对所有是 的倍数的数字们组成的数组求逆序对即可,实现上我们可以开一个二维,其中 存了所有排列值是 的倍数的排列值,按原排列的下标顺序存。
举个例子:如果排列是:,则 ,。
接着我们只需要对所有的 都求一遍逆序对,就得到了初始的 。而一个数组内求逆序对这一过程我们可以使用树状数组做到 的时间复杂度,其中 是数组长度。
得到初始的 后再做上述的容斥,我们就解决了本问题。
代码
class BIT {
public:
BIT(int size) : size_(size), tree_(size + 1, 0) {}
void update(int index, int delta) {
for (int i = index + 1; i <= size_; i += (i & -i)) {
tree_[i] += delta;
}
}
int query(int index) {
int sum = 0;
for (int i = index + 1; i > 0; i -= (i & -i)) {
sum += tree_[i];
}
return sum;
}
int queryRange(int left, int right) {
return query(right) - query(left - 1);
}
private:
int size_;
vector<int> tree_;
};
vector<vector<int>> d(202020);
void init() {
int n = 2e5;
for(int i = 1; i <= n; i++) {
for(int j = i; j <= n; j += i) {
d[j].emplace_back(i);
}
}
}
void solve() {
int n;
cin >> n;
vector<vector<int>> g(n + 1);
for(int i = 1, x; i <= n; i++) {
cin >> x;
for(auto & fac : d[x]) {
g[fac].emplace_back(x);
}
} // 处理完这一步,g 就是上文提到的二维 vector
vector<int> dp(n + 1);
for(int i = 1; i <= n / 2; i++) { // 读者可以思考这里为什么只枚举到 n/2,而不是 n
int sz = n / i;
BIT b(sz + 1);
for(int j = g[i].size() - 1; ~j; j--) {
int now = g[i][j] / i;
dp[i] += b.query(now - 1);
b.update(now, 1);
}
}
for(int i = n / 2; i; i--) {
for(int j = i + i; j <= n; j += i) {
dp[i] -= dp[j];
}
}
cout << dp[1] << endl;
}
/*
*/
signed main () {
// init(minp, primes, m); // primes
init();
int t = 1;
// cin >> t;
// getchar();
while(t -- ) {
solve();
}
return 0;
}
当然本题也可以通过预处理莫比乌斯函数 后再做容斥的方式通过,和上文给出的做法本质完全相同,这里不再赘述。