感觉难度不是顺序的,感觉是 B < C = E = F < G < A < D < H
A
显然是一道01背包的变体,把不选择
改成加上lost[i]
即可
关键代码
for (int i = 1; i <= n; ++ i) {for (int j = x; j >= 0; -- j) {if (j >= u[i]) f[j] = max(f[j] + l[i], f[j - u[i]] + w[i]);else f[j] += l[i];}
}
B
用高精度-单精度乘法即可,也没什么好讲的,具体原理可以去 OI-Wiki 学习
关键代码
void mul_short(vector<int> &a, int b) {for (int i = 0; i < a.size(); ++i) {a[i] *= b;if (i) {a[i] += a[i-1] / 10;a[i-1] %= 10;}}while (a.back() >= 10) {int tmp = a.back() / 10;a.back() %= 10;a.push_back(tmp);}
}
C
对于第 \(i\) 头牛,查找左右最远的能听到声音的牛,显然可以用二分找到对应区间 \([l,\ r]\)
关键代码
#define all(a) a.begin(), a.end()for (int i = 0; i < n; i ++) {int l = lower_bound(all(p), p[i]-d) - p.begin();int r = upper_bound(all(p), p[i]+d) - p.begin() - 1;ans += r - l;
}
D
问题描述很清晰,比较考验积累,如果知道相关公式就很简单,难度较高是因为可能很多人不记得,需要现推
第二类斯特林数
可记做 \(S(n,k)\) ,表示将 \(n\) 个两两不同的元素,划分为 \(k\) 个互不区分的非空子集的方案数。
递推式
边界值就是 \(S(n, k) = 1\ [n = k]\) ,\(S(n, k) = 0\ [m=0或m>n]\)
通项公式
展示一下递推式的写法
int S(int n, int m) {if (n == m) return 1;if (m == 0 || m > n) return 0;return m * S(n-1, m) + S(n-1, m-1);
}
现在算出不区分箱子的情况了,那么答案就是 \(S(n, r) * r!\)
E
数据范围很小,所以暴力即可,枚举每一个城市算出总路费,取最小值即可
关键代码
struct city {int cnt, d;string name;bool operator < (const city &other) const {return d < other.d;}
};
-----------------------------------------------------
city ans{inf, 0, ""};
for (int i = 0; i < n; i ++) {int sum = 0;for (int j = 0; j < n; j ++) {sum += abs(c[i].d - c[j].d) * c[j].cnt;}if (sum < ans.cnt) {ans = {sum, 0, c[i].name};}
}
F
不懂为什么叫增强版,数据范围也不大,纯模拟即可
算出总共种了几棵树苗,最后看看剩下几颗树苗,减一下就好
关键代码
vector<pii> tr(L + 1, {1, 0});
int sum{ 0 }, cnt{ 0 };
while (n --) {int op, l, r;cin >> op >> l >> r;for (int i = l; i <= r; i ++) {if (op) {if (!tr[i].first) {sum ++;tr[i].second = 1;}tr[i].first = 1;} else {tr[i] = {0, 0};}}
}
for (int i = 0; i <= L; ++ i) {cnt += tr[i].second;
}
G
没啥好说的,01背包板子题,感觉接触过背包的都见过这题了,不了解原理的可以取洛谷搜索“背包九讲”,非常详细
关键代码
vector<int> w(n+1), v(n+1), f(W+1);
for (int i = 1; i <= n; ++ i) {for (int j = W; j >= w[i]; -- j) {f[j] = max(f[j], f[j-w[i]] + v[i]);}
}
cout << f[W];
H
本场最有难度的一题
首先你要知道如何求 最大子段和
原理网上有很多详细讲解,这里贴一下代码
int ans = -inf, sum = 0;
for (int i = 0; i < n; i ++) {sum = max(sum + a[i], a[i]);ans = max(ans, sum);
}
\(ans\) 即为数组 \(a\) 的最大子段和,这里要求子段不能为空,如果可以为空,则 \(ans\) 初始值修改为 \(0\)
然后进阶到 环状最大子段和
成环后,子段可能有两种情况("-"表示对应区间)
- 区间不跨端点:........---.....
- 跨端点:---.........--
第一种情况可以直接求最大子段和,第二种应该怎么搞呢?
假设数列 \(a\) 和为 \(SUM\) 最大子段和为 \(sum\) ,那么显然有 \(SUM - sum\) 是 最小子段和,如果我们知道了最小子段和,也就可以算出第二种情况下的最大子段和
最小子段和怎么求呢?我们只需要把数组 \(a\) 全部取相反数,这时再算最大子段和,那对应的就是原数组的最小子段和。
现在单个区间的最大子段和会算了,那就可以求两个区间了
首先思考可能的区间分布,跨端点的区间显然最多存在一个
- .....---.......-----........
- ---........----......----
那么我们维护两个数组 \(f_i\) 和 \(g_i\)
- \(f_i\) :\([1, i]\) 的最大子段和
- \(g_i\) :\([i, n]\) 的最大子段和
然后就可以 \(O(n)\) 的计算 \(f_i+g_{i+1}\) 取最大值
那么在正常情况下我们可以求第一种分布的答案
取相反数的情况下,用 \(SUM - sum\) 我们可以求第二种分布的答案
特例:如果只有一个正数,那么取相反数后就不会得到正确答案,但这中情况下直接当做第一种情况处理即可(即选择正数和绝对值最小的负数,区间长度为1,一定不会绕回起点)
关键代码
int maxsum() {for (int i = 1; i <= n; ++ i)f[i] = max(f[i-1] + a[i], a[i]);for (int i = 1; i <= n; ++ i)f[i] = max(f[i-1], f[i]);for (int i = n; i > 0; -- i)g[i] = max(g[i+1] + a[i], a[i]);for (int i = n; i > 0; -- i)g[i] = max(g[i+1], g[i]);int res{ -inf };for (int i = 1; i < n; i ++)res = max(res, f[i] + g[i+1]);return res;
};
-------------------------------------------------------
int ans = maxsum();
if (cnt == 1) {//正数数量为1cout << ans;
} else {for_each(all(a), [](int &x){x = -x;}); // 对每个元素取相反数sum += maxsum(); // 取反后的最大子段和相当于 (-1 * 元数组对应子段和)// 总和去掉这一区间就是 sum - (-1 * 元数组对应子段和),所以最后是相加if (!sum) cout << ans; // 如果删掉整个数组,那么直接舍去取相反后的结果else cout << max(sum, ans);
}