HD 2025 春季联赛 4
1001
模拟
就注意理解一下题意,题中 \(k\) 的含义是当一个敌人收到了 \(k\) 次伤害后,如果没死,就进入了不可选中的状态。其他的就没啥了直接看码。
CODE
struct ENEMY {
int hp, u, idx, cnt;
bool operator< (const ENEMY &a) const {
if (hp != a.hp) {
return hp > a.hp;
}
if (u != a.u) {
return u > a.u;
}
return idx > a.idx;
}
};
void solve()
{
int n, u, k, hp;
std::cin >> n >> u >> k >> hp;
std::priority_queue<ENEMY> attack;
std::priority_queue<std::pair<int, int>> mx;
std::vector dead(n + 1, false);
for (int i = 1; i <= n; i++) {
ENEMY tmp;
std::cin >> tmp.u >> tmp.hp;
tmp.idx = i, tmp.cnt = 0;
attack.push(tmp);
mx.push(std::make_pair(tmp.u, i));
}
int ans = 0;
while (hp >= 0 && ans < n) {
auto cur = attack.top();
attack.pop();
cur.hp -= cur.cnt == 0 ? u : (u >> 1);
cur.cnt++;
if (cur.hp <= 0) {
dead[cur.idx] = true;
ans++;
}
else if (cur.cnt < k) {
attack.push(cur);
}
while (not mx.empty() && dead[mx.top().second]) {
mx.pop();
}
if (not mx.empty()) {
hp -= mx.top().first;
}
}
std::cout << ans << '\n';
}
1002
还没看……
1003
二进制
解题思路
二进制下两个数比较大小,我们从高位往低位看,若相同则往下看,否则就能判断大小了。该题的做法就是基于这个简单直白的性质。
首先我们可以判断 \(x\) 的上界,即使 \(p_x = kx + b\) 的第 61 位为 1 的 \(x\),因为 \(10^{18} < 2^{60} < 2^{61}\)。下界当然就是 0 了。得到了上下界 \([l, r]\),我们就可以从高位往低位逐渐缩小我们的范围 \([l, r]\),并在此过程中计算答案。具体的:
- 在最高位第 61,我们可以找到两个相邻的数 \(ll\) 和 \(rr\) 使得对于 \(x \in [l, rr]\), \(p_x\) 的第 61 位为 0,对于 \(x \in [l, r]\), \(p_x\) 的第 61 位为 1。此时,\(c\) 和 \(x\) 的第 61 位都为0,所以为了使 \(p_x \oplus c \leq v\),我们将区间缩至 \([l, rr]\) 让 \(p_x \oplus c\) 的第 61 位为 1。
- 从高位到底位我们可以不断缩减区间,因为只要我们的 \(x\) 在区间内,就一定可以保证高位不变而只变低位。与最高位第 61 位有些许不同的是,在较低位,我们的 \(c\) 和 \(v\) 可能是 1。\(c\) 在当前遍历到的位是 1 与否,只会影响我们选择 \([l, rr]\) 和 \([ll, r]\) 中的那个区间。而 \(v\) 在当前遍历到的位是 1 时,我们就可以把使得 \(p_x \oplus c\) 的当前位位 0 的区间的长度直接加到答案里去,因为在这个区间里,\(p_x \oplus c\) 和 \(v\) 在较高位是相等的,而在当前位 \(p_x \oplus c < v\),所以不管低位如何整体的值就 \(p_x \oplus c < v\)。
- 最后注意以下把遍历完后的区间长度加到答案里面去就好了。
CODE
void solve()
{
i64 k = 0, b = 0, c = 0, v = 0;
std::cin >> k >> b >> c >> v;
i64 ans = 0, l = 0, r = (1ll << 61) / k;
for (int bit = 61; ~bit; bit--) {
i64 ll = l, rr = r;
while (ll <= rr) {
i64 m = ll + rr >> 1;
if ((k * m + b) >> bit & 1) {
rr = m - 1;
}
else {
ll = m + 1;
}
}
std::array<i64, 2> rng[2] = { { l, rr }, { ll, r } };
int cur = c >> bit & 1;
if (v >> bit & 1) {
ans += rng[cur][1] - rng[cur][0] + 1;
l = rng[cur ^ 1][0];
r = rng[cur ^ 1][1];
}
else {
l = rng[cur][0];
r = rng[cur][1];
}
}
std::cout << ans + (r - l + 1) << '\n';
}
1004
代码很简答,但是还没想明白具体是怎么个事
CODE
int ans;
void dfs(int cur, i64 gcd) {
if (g[cur].empty()) {
if (__builtin_popcount(gcd) <= 1) {
ans++;
}
return;
}
for (auto &to : g[cur]) {
dfs(to, std::__gcd(gcd, std::abs(a[to] - a[cur])));
}
}
void solve()
{
int n = 0;
std::cin >> n;
for (int i = 1; i <= n; i++) {
g[i].clear();
}
for (int i = 2; i <= n; i++) {
int f = 0;
std::cin >> f;
g[f].push_back(i);
}
for (int i = 1; i <= n; i++) {
std::cin >> a[i];
}
ans = 0;
dfs(1, 0);
std::cout << ans << '\n';
}
1005
贪心
解题思路
首先对于打折和减价,我们肯定先选择打折,而且在打折时,我们肯定先使用折扣力度大的;在减价时,我们肯定先使用减价多的。
于是排个序前缀搞一下然后枚举用几次打折就好了取最小值就好了。
CODE
void solve()
{
int p = 0, n = 0, k = 0;
std::cin >> p >> n >> k;
for (int i = 0; i <= n; i++) {
mul[i] = 1.0,
sub[i] = 0;
}
int c0 = 0, c1 = 0;
for (int i = 0; i < n; i++) {
int op = 0, x = 0;
std::cin >> op >> x;
if (op == 0) {
mul[++c0] = 1.0 * x / 10.0;
}
else {
sub[++c1] = x;
}
}
std::sort(mul + 1, mul + 1 + n);
std::sort(sub + 1, sub + 1 + n, std::greater<i64>());
for (int i = 1; i <= n; i++) {
mul[i] *= mul[i - 1];
sub[i] += sub[i - 1];
}
double ans = p;
for (int i = 0; i <= k; i++) {
ans = std::min(ans, 1.0 * mul[i] * p - sub[k - i]);
}
if (ans < 0.0) {
ans = 0;
}
std::cout << std::fixed << std::setprecision(2) << ans << '\n';
}
1006
树状数组
树状数组秒了
CODE
int n = 0, q = 0;
int a[N + 5];
i64 tr[N + 5];
int lowbit(int x) {
return x & -x;
}
void add(int pos, i64 val) {
while (pos <= n) {
tr[pos] += val;
pos += lowbit(pos);
}
return;
}
i64 ask(int pos) {
i64 res = 0;
while (pos >= 1) {
res += tr[pos];
pos -= lowbit(pos);
}
return res / 100;
}
void solve()
{
std::cin >> n >> q;
for (int i = 1; i <= n; i++) {
tr[i] = 0;
}
for (int i = 1; i <= n; i++) {
std::cin >> a[i];
add(i, a[i]);
}
i64 ans = 0;
int tim = 0;
while (q--) {
int op, x, y;
std::cin >> op >> x >> y;
if (op == 1) {
add(x, y - a[x]);
a[x] = y;
}
else {
ans ^= 1ll * (++tim) * (ask(y) - ask(x - 1));
}
}
std::cout << ans << '\n';
return;
}
1007
博弈论 NP状态 模拟
解题思路
这道题不是思维博弈,而是需要你去模拟这个游戏然后确定每个游戏局面的状态的。
对于 N态和P态,我们要知道以下几点:
- N(Next)态代表后手必胜态,P(precious)态代表先手必胜态。
- 对于次态存在N态的状态是P态。
- 对于次态中都是P态的状态是N态。
在这题中有显而易见的是N态或者P态的局面,即后两个数是 0 或者前两个数是 0 的局面。
我们可以从这些已经确定的局面出发BFS去考虑那些能够到达这些局面的局面,并不断将确定状态的局面入队。
确定了所有局面的状态后,就可以直接输出答案了。
(此题的代码并不怎么好写)
CODE
// 0 : Uncertain, AKA Tie
// 1 : P
// 2 : N
int status[N];
int cntp[N]; // 统计次态有多少是 P态
std::vector<int> g[N];
std::vector<int> ig[N];
int idx[10][10][10][10];
void solve()
{
int a = 0, b = 0, c = 0, d = 0;
std::cin >> a >> b >> c >> d;
if (a > b) {
std::swap(a, b);
}
if (c > d) {
std::swap(c, d);
}
std::cout << status[idx[a][b][c][d]] << '\n';
}
int main()
{
IOS;
int _t = 1;
std::cin >> _t;
std::queue<int> q;
auto push = [&](int u, int s) -> void {
status[u] = s;
q.push(u);
};
auto add = [&](int u, int a, int b, int c, int d) -> void {
if (c > d) {
std::swap(c, d);
}
int v = idx[a][b][c][d];
assert(a <= b && c <= d);
g[u].push_back(v);
ig[v].push_back(u);
};
int cnt = 0;
// 显然我们有许多等价的局面,如 { a, b, c, d} 和 { b, a, d, c }
// 此时我们就只取一种
// 这一段是考虑所有可能到达的不等价的局面并标号
for (int a = 0; a < 10; a++) {
for (int b = a; b < 10; b++) {
for (int c = 0; c < 10; c++) {
for (int d = c; d < 10; d++) {
int u = idx[a][b][c][d] = cnt++;
if (a == 0 && b == 0) {
push(u, 1);
}
else if (c == 0 && d == 0) {
push(u, 2);
}
}
}
}
}
// 这一段是考虑所有未分出胜负的局面并寻找其次态
for (int a = 0; a < 10; a++) {
for (int b = std::max(a, 1); b < 10; b++) {
for (int c = 0; c < 10; c++) {
for (int d = std::max(c, 1); d < 10; d++) {
int u = idx[a][b][c][d];
// 注意在模拟时,玩家不能使用 0,也不能去使用对方的 0。
if (a) {
if (c) {
add(u, c, d, (a + c) % 10, b);
}
add(u, c, d, (a + d) % 10, b);
}
if (c) {
add(u, c, d, a, (b + c) % 10);
}
add(u, c, d, a, (b + d) % 10);
}
}
}
}
while (not q.empty()) {
int v = q.front();
q.pop();
if (status[v] == 2) { // 能达到N态的状态就直接是P态。
for (auto &u : ig[v]) {
if (status[u] == 0) {
push(u, 1);
}
}
}
else {
for (auto &u : ig[v]) { // 所有次态都是P态的就是N态
if (status[u] == 0 && ++cntp[u] == g[u].size()) {
push(u, 2);
}
}
}
}
while (_t--)
{
solve();
}
sp();
return 0;
}
1008
DP
不解释直接看码
CODE
void solve()
{
int n = 0, k = 0;
std::cin >> n >> k;
std::vector a(n + 1, std::vector(k + 1, 0));
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= k; j++) {
std::cin >> a[i][j];
}
}
std::vector f(n + 1, std::vector(k + 1, 0));
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= k; j++) {
f[i][j] = f[i - 1][j] + a[i][j];
}
for (int j = 1; j <= k; j++) {
f[i][j] = std::max(f[i][j], f[i][j - 1]);
}
}
std::cout << f[n][k] << '\n';
return;
}
1009
状态压缩
解题思路
首先我们可以用一个 \(n\) 位二进制数来表示有那些英雄可以选。在这个二进制数中,第 \(i\) 位为 1 代表第 \(i\) 位英雄可以选择。于是对于所有的 \(s \in [1,2^n)\) 如果我们提前计算出 \(ans[s]\) 表示可选英雄构成二进制数 \(s\) 时的方案数,就可以快速地回答询问。
首先我们计算出只选择 5 个英雄的方案数,具体如下:
先计算在不考虑分路的情况下,这 5 个人的英雄池能有那些组合,然后再考虑分路,每种英雄组合有共 \(5!\) 种不同的分路选择,看有几种是合法的,如果是 0 种就说明这个英雄组合是不合法的。
得出这个后我们就可以 SOSdp 跑一下算出整个 \(ans\) 数组了。
CODE
int n, q;
std::vector<int> use[5];
int R[N][5];
int ans[1 << N];
void solve()
{
std::cin >> n >> q;
for (auto &v : use) {
int m = 0;
std::cin >> m;
v.clear();
v.assign(m, 0);
for (auto &i : v) {
std::cin >> i;
i--;
}
}
std::fill(ans, ans + (1 << n), 0);
for (auto &c0 : use[0]) {
for (auto &c1 : use[1]) {
for (auto &c2 : use[2]) {
for (auto &c3 : use[3]) {
for (auto &c4 : use[4]) {
ans[(1 << c0) | (1 << c1) | (1 << c2) | (1 << c3) | (1 << c4)]++;
}
}
}
}
}
for (int i = 0; i < n; i++) {
for (int j = 0; j < 5; j++) {
std::cin >> R[i][j];
}
}
std::vector p(5, 0), b(0, 0);
std::iota(p.begin(), p.end(), 0);
for (int s = 0; s < (1 << n); s++) {
if (std::__popcount(s) != 5) {
ans[s] = 0;
}
else {
b.clear();
for (int i = 0; i < n; i++) {
if (s >> i & 1) {
b.push_back(i);
}
}
int mul = 0;
do
{
if (R[b[0]][p[0]] && R[b[1]][p[1]] && R[b[2]][p[2]] && R[b[3]][p[3]] && R[b[4]][p[4]]) {
mul++;
}
} while (std::next_permutation(p.begin(), p.end()));
ans[s] *= mul;
}
}
// SOS dp
for (int i = 0; i < n; i++) {
for (int s = 0; s < (1 << n); s++) {
if (s >> i & 1) {
ans[s] += ans[s ^ (1 << i)];
}
}
}
while (q--) {
int m = 0, ban = 0;
std::cin >> m;
for (int i = 0; i < m; i++) {
int x = 0;
std::cin >> x;
ban |= (1 << x - 1);
}
ban ^= (1 << n) - 1;
std::cout << ans[ban] << '\n';
}
return;
}
1010
根号分块
解题思路
我们把给出的序列认为是给节点染色了,序列中的每个值就代表一个颜色。我们并不需要真的按照题目的要求在节点中连边建图,我们只用在颜色之间连边建图就好了。
建好图之后我们就容易想到一种暴力的 DP 做法,即从 1 到 \(n\),每次计算时不仅要计算 dp 数组的答案,还要将计算出来的答案存储到当前的颜色中,于是转移数组中我们只有找与当前颜色连接的颜色的值就好了:
{
for (int i = 1; i <= n; i++) {
for (auto &to : g[i]) {
dp[i] += cnt[to];
}
}
cnt[a[i]] += dp[i];
}
这样做的时间复杂度在最坏的情况下是 \(O(NC)\) 的不能通过此题。想要优化整个复杂度,需要知道在一个有 \(m\) 条边的图中,节点度数大于 \(k\) 的节点有 \(m \over k\) 个(考虑每一个度数大于 \(k\) 的节点,就算每个点只有 \(k\) 边, \({m \over k}\) 个这样的节点就有差不多 \(m\) 条边了)。于是我们尝试分块:
- 对于度数小于等于 \(k\) 的节点,我们就用以上暴力。
- 对于度数大于 \(k\) 的节点(记为大点),我们建立这些节点的反图,在 dp 计算答案的过程中,每次都将当前答案加到当前颜色能到达的大点上,要用是就直接取数就好了。
这样做整体的复杂度是 \(O(n(k + {m \over k}))\) 的,显然在 \(k = \sqrt{m}\) 时最优。
CODE
int a[N + 5];
std::vector<int> g[N + 5], h[N + 5];
i64 cnt[N + 5], pre[N + 5];
void solve()
{
int n = 0, c = 0;
std::cin >> n >> c;
for (int i = 1; i <= n; i++) {
std::cin >> a[i];
}
int m = 0;
std::cin >> m;
for (int i = 1; i <= c; i++) {
g[i].clear(), h[i].clear();
cnt[i] = pre[i] = 0;
}
for (int i = 0; i < m; i++) {
// 1 <= u, v <= c
int u = 0, v = 0;
std::cin >> u >> v;
g[u].push_back(v);
if (u != v) {
g[v].push_back(u);
}
}
int k = std::sqrt(c);
for (int i = 1; i <= c; i++) {
if (g[i].size() > k) {
for (auto &to : g[i]) {
h[to].push_back(i);
}
}
}
for (int i = 1; i <= n; i++) {
i64 cur = (i == 1 ? 1 : 0);
int col = a[i];
if (g[col].size() > k) {
(cur += pre[col]) %= Mod;
}
else {
for (auto &to : g[col]) {
(cur += cnt[to]) %= Mod;
}
}
if (i == n) {
std::cout << cur << '\n';
return;
}
(cnt[col] += cur) %= Mod;
for (auto &to : h[col]) {
(pre[to] += cur) %= Mod;
}
}
}
浙公网安备 33010602011771号