2024“钉耙编程”中国大学生算法设计超级联赛(7)
写在前面
补提地址:https://acm.hdu.edu.cn/listproblem.php?vol=66,题号 7505~7516。
以下按个人向难度排序。
还是单刷,罚上天了被新生打爆了哈哈。
什么爬塔场呃呃
1010
贪心,签到。
打的敌人是一段连续的前缀,于是考虑枚举前缀长度,在此过程中最大化打完某个前缀时剩余血量的最大值和烟雾弹次数。
发现扔烟雾弹当且仅当出现某个敌人 \(a_i\) 不小于当前血量时。此时应当选择的操作是:
- 直接对当前敌人扔烟雾弹。
- 选择将对之前直接开打的某个敌人的策略替换为扔烟雾弹,并增加血量以应对当前敌人。
上述过程显然是一个反悔贪心的形式。考虑使用优先队列维护在此之前直接开打的敌人的血量的最值,当出现某个敌人 \(a_i\) 不小于当前血量时弹出其中的最大值,将对该敌人的策略替换为扔烟雾弹即可。
总时间复杂度 \(O(n\log n)\) 级别。
//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define int long long
const int kN = 1e5 + 10;
//=============================================================
int n, x, k, a[kN];
//=============================================================
//=============================================================
signed main() {
// freopen("1.txt", "r", stdin);
std::ios::sync_with_stdio(0), std::cin.tie(0);
int T; std::cin >> T;
while (T --) {
std::cin >> n >> x >> k;
for (int i = 1; i <= n; ++ i) std::cin >> a[i];
int ans = 0;
std::priority_queue <int> q;
for (int i = 1; i <= n; ++ i) {
if (x <= a[i] && k) {
if (!q.empty() && a[i] < q.top()) {
while (x <= a[i] && k && !q.empty()) {
-- k;
x += q.top();
q.pop();
}
}
}
if (x > a[i]) {
x -= a[i];
q.push(a[i]);
} else if (k) {
-- k;
//break;
} else {
break;
}
ans = i;
}
std::cout << ans << "\n";
}
return 0;
}
1011
签到,讨论。
先特判可以一刀不切,和仅切草莓的情况。
然后考虑分别对草莓和蛋糕切 \(n, m\) 刀,则此时有:
- \(2nx \ge y\),或 \(2nx \ge 2my(m>1)\)
- \(y | 2nx\),或 \(2my | 2nx(m>1)\)
在此基础上需要最小化 \(n\),再最小化 \(m\)。为了保证 \(2nx\) 为 \(y\) 的倍数,则显然 \(n\) 取最小值时,显然最优的情况是:
于是考虑判断 \(\frac{y}{\gcd(x, y)}\) 的奇偶性以判断上式是否可以成立。若为偶数则可直接令 \(2n = \frac{y}{\gcd(x, y)}\),否则 \(2n = \frac{2y}{\gcd(x, y)}\)。
显然一定有 \(y | 2nx\),为了保证蛋糕最大,则应当蛋糕一刀不切,即可直接计算出草莓的数量。
//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define i128 __int128
#define LL long long
//=============================================================
//=============================================================
void write(i128 x_) {
if (x_ < 0) putchar('-'), x_ = -x_;
if (x_ > 9) write(x_ / 10);
putchar(x_ % 10 + '0');
}
i128 gcd(i128 x_, i128 y_) {
return (y_) ? gcd(y_, x_ % y_) : x_;
}
//=============================================================
int main() {
// freopen("1.txt", "r", stdin);
// std::ios::sync_with_stdio(0), std::cin.tie(0);
int T; std::cin >> T;
while (T --) {
// std::cin >> x >> y;
LL x, y;
scanf("%lld%lld", &x, &y);
if (x % y == 0) {
write(y);
putchar(' ');
write(x / y);
putchar('\n');
continue;
} else if (y % (2 * x) == 0) {
write(y);
putchar(' ');
write(1);
putchar('\n');
continue;
}
i128 n = y / gcd(x, y);
i128 km = n * x / y, ans1, ans2;
if (n % 2 == 0) {
ans1 = y, ans2 = km;
} else {
ans1 = y, ans2 = 2ll * km;
}
write(ans1);
putchar(' ');
write(ans2);
putchar('\n');
}
return 0;
}
/*
1
26 3
*/
1009
枚举,搜索
保证一种物品只会作为一个配方的原料,以及一种配方的产物,即保证所有点出度为 1;又保证一定可以在有限的时间内生产出最终产物,即保证无环,则实际上构成了一棵树,可以简单地计算出无赠送物品情况下,每种物品合成的耗时。
显然获取赠送物品时,一定会选择直接参与最终产物的合成的物品。则显然最优的选择是选择其中对最终产品影响最大的,答案即最终产物合成时间减去该物品的影响。发现按顺序合成物品时,所需的可以直接获取的原材料的数量是指数级递增的,则实际上算耗时会很快超过 \(10^9\) 这个数量级,于是考虑直接从最终物品开始反向大力搜索,求得每个物品的耗时,在此过程中若超过 \(10^9\) 则直接返回 -1
即可。
若直接参与最终产物的合成的物品中,有超过两个 -1
则直接无解,否则检查删掉影响最大的物品后答案是否超过 \(10^9\) 即可。
保证每个物品只会被搜索一次,总时间复杂度 \(O(n)\) 级别。
//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
const int kN = 1e5 + 10;
const LL kInf = 1e10 + 2077;
const LL kI = 1e9;
//=============================================================
int n, k;
int t[kN], into[kN];
std::vector<int> from[kN], need[kN];
LL f[kN];
//=============================================================
LL dfs(int u_) {
if (t[u_] != 0) return t[u_];
LL sum = 0;
for (int i = 0, sz = from[u_].size(); i < sz; ++ i) {
LL ret = dfs(from[u_][i]);
LL need_ = need[u_][i];
if (ret == -1) return -1;
sum += need_ * ret;
if (sum > kI) return -1;
}
return sum;
}
//=============================================================
int main() {
// freopen("1.txt", "r", stdin);
std::ios::sync_with_stdio(0), std::cin.tie(0);
int T; std::cin >> T;
while (T --) {
std::cin >> n >> k;
for (int i = 1; i <= n; ++ i) t[i] = 0, from[i].clear(), need[i].clear(), into[i] = 0;
for (int i = 1; i <= n; ++ i) {
int p; std::cin >> p;
if (p == 0) std::cin >> t[i];
if (p == 1) {
int cnt; std::cin >> cnt;
for (int j = 1; j <= cnt; ++ j) {
int x, a; std::cin >> x >> a;
from[i].push_back(a);
need[i].push_back(x);
++ into[a];
}
}
}
if (t[k] != 0) {
std::cout << t[k] << "\n";
continue;
}
int cnt = 0;
LL ans = 0;
std::vector<LL> fson;
for (int i = 0, sz = from[k].size(); i < sz; ++ i) {
LL ret = dfs(from[k][i]);
LL need_ = need[k][i];
if (ret == -1) {
++ cnt;
continue;
}
ans += need_ * ret;
fson.push_back(need_ * ret);
}
std::sort(fson.begin(), fson.end());
if (cnt == 0) ans -= fson.back();
else if (cnt > 1) ans = kI + 1;
if (ans <= kI) std::cout << ans << "\n";
else std::cout << "Impossible" << "\n";
}
return 0;
}
1004
手玩,结论。
大力手玩题,考虑何种情况下防守方有必胜策略。
首先显然应当有 \(r_2 \ge 2\times r_1 + 1\),否则防守方单次移动无法跨越整个轰炸范围,会导致一步一步被逼向叶节点直至无处可逃。
然后发现防守方仅需到达一条长度至少为 \(2\times r_1 + 1\) 的链上即可,此时防守方即可在这条链上反复横跳。又树上最长链为直径,于是需要保证直径长度 \(\ge 2\times r_1 + 1\)。
然后考虑防守方初始位置的影响,显然第一步移动范围一定需要不小于 \(r_1 + 1\),否则进攻方第一步直接轰炸 \(s\) 即可获胜。考虑到对于树上任意点,与其距离最远的点一定是直径的某端点,则仅需是否有判断 \(s\) 与直径某端点距离 \(\ge r_1 + 1\) 即可。发现在此之后的每一步均位于直径上/位于一条长度不小于 \(2\times r_1 + 1\) 的链上,可以保证每次都能逃出包围圈。
综上所述,当且仅当如下三个条件同时成立时,防守方有必胜策略:
- \(r_2 \ge 2\times r_1 + 1\);
- 直径长度 \(\ge 2\times r_1 + 1\);
- \(s\) 与直径某端点距离 \(\ge r_1 + 1\)。
//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
const int kN = 2e6 + 10;
//=============================================================
int n, s, r1, r2;
int edgenum, head[kN], v[kN << 1], ne[kN << 1];
int from[kN], dis[kN];
//=============================================================
void addedge(int u_, int v_) {
v[++ edgenum] = v_;
ne[edgenum] = head[u_];
head[u_] = edgenum;
}
void dfs(int u_, int fa_, bool flag) {
if (flag) from[u_] = fa_;
dis[u_] = dis[fa_] + 1;
for (int i = head[u_]; i; i = ne[i]) {
int v_ = v[i];
if (v_ == fa_) continue;
dfs(v_, u_, flag);
}
}
bool solve() {
int road[2] = {s, s}, maxdis = 0, maxdiss = 0;
dfs(road[0], 0, 0);
for (int i = 1; i <= n; ++ i) if (dis[i] > maxdis) road[0] = i, maxdis = dis[i];
maxdiss = maxdis;
dfs(road[0], 0, 1);
maxdis = 0;
for (int i = 1; i <= n; ++ i) if (dis[i] > maxdis) road[1] = i, maxdis = dis[i];
return ((maxdiss - 1) >= (r1 + 1)) && ((maxdis - 1) >= 2 * r1 + 1);
}
//=============================================================
int main() {
//freopen("1.txt", "r", stdin);
std::ios::sync_with_stdio(0), std::cin.tie(0);
int T; std::cin >> T;
while (T --) {
std::cin >> n >> s >> r1 >> r2;
edgenum = 0;
for (int i = 1; i <= n; ++ i) head[i] = dis[i] = 0;
for (int i = 1; i < n; ++ i) {
int u_, v_; std::cin >> u_ >> v_;
addedge(u_, v_), addedge(v_, u_);
}
if (r2 < 2 * r1 + 1) {
std::cout << "Kangaroo_Splay" << "\n";
continue;
}
if (solve()) std::cout << "General_Kangaroo\n";
else std::cout << "Kangaroo_Splay\n";
}
return 0;
}
1007
枚举,DP
勾八数据范围,赛时把我骗去想什么 \(O(q^2\log n + n\log^2 n)\) 的分治呃呃
首先要求删去尽可能少,转化为保留尽可能多,则对于单次询问实际上即求该区间内一个最长的子序列,满足相邻元素绝对值不超过 \(k\)。则有一个非常显然的大力 DP,记 \(f_i\) 表示以 \(a_i\) 结尾的最长的合法的子序列的长度,初始化 \(\forall l\le i\le r, f_i = 1\),则有转移:
对于上述状态转移方程,发现有贡献的位置的 \(a_j\) 是一段连续的权值区间,一个非常套路的做法是使用权值线段树进行维护,权值线段树叶节点 \([i, i]\) 维护结尾为权值 \(i\) 的子序列的最大长度,并维护区间最大值,转移时直接查询对应区间最大值即可。单次转移时间复杂度为 \(O(\log n)\) 级别,则总时间复杂度为 \(O(Tqn\log n)\) 级别,被卡了过不去呃呃呃呃
于是显然不能转移的时候用数据结构直接维护,于是考虑能否预处理最优决策,即可每次 \(O(1)\) 地转移。考虑换成刷表法从前往后地进行转移,则上述状态转移方程可写成:
发现每次从 \(i\) 转移到 \(j\) 的最优决策,一定是转移到距离 \(i\) 最近的,第一个满足 \(a_j - k\le a_i\) 或 \(a_i \le a_j + k\) 的位置。否则若转移到非最近的位置 \(j'\),由于有 \(i < j < j'\) 则一定可以在选择的子序列的 \(a_i, a_{j'}\) 中插入 \(a_j\) 使得子序列仍合法的同时,使得答案更优。
考虑在询问之前,倒序枚举位置 \(i\) 过程中,维护权值线段树用于预处理距离 \(i\) 最近的,第一个满足 \(a_j - k\le a_i\) 或 \(a_i \le a_j + k\) 的位置,则询问时每次转移的时间复杂度变为 \(O(1)\) 级别,总时间复杂度为 \(O\left(T(n\log n + qn)\right)\) 级别。
//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
const int kN = 1e5 + 10;
//=============================================================
int n, datanum, next[kN][2], f[kN];
LL m, k, a[kN], b[kN], c[kN], data[kN];
//=============================================================
namespace Seg {
#define ls (now_<<1)
#define rs (now_<<1|1)
#define mid ((L_+R_)>>1)
const int kNode = kN << 2;
int mina[kNode], tag[kNode];
void Pushup(int now_) {
mina[now_] = std::min(mina[ls], mina[rs]);
}
void Pushdown(int now_) {
mina[ls] = mina[rs] = kN;
tag[ls] = tag[rs] = 1;
tag[now_] = 0;
}
void modify(int now_, int L_, int R_, int pos_, int val_) {
if (L_ == R_) {
mina[now_] = val_;
return ;
}
if (tag[now_]) Pushdown(now_);
if (pos_ <= mid) modify(ls, L_, mid, pos_, val_);
else modify(rs, mid + 1, R_, pos_, val_);
Pushup(now_);
}
int query(int now_, int L_, int R_, int l_, int r_) {
if (l_ <= L_ && R_ <= r_) return mina[now_];
if (tag[now_]) Pushdown(now_);
int ret = kN;
if (l_ <= mid) ret = std::min(ret, query(ls, L_, mid, l_, r_));
if (r_ > mid) ret = std::min(ret, query(rs, mid + 1, R_, l_, r_));
return ret;
}
void clear() {
mina[1] = kN;
tag[1] = 1;
}
#undef ls
#undef rs
#undef mid
}
//=============================================================
int main() {
// freopen("1.txt", "r", stdin);
std::ios::sync_with_stdio(0), std::cin.tie(0);
int T; std::cin >> T;
while (T --) {
std::cin >> n >> m >> k;
for (int i = 1; i <= n; ++ i) std::cin >> a[i];
for (int i = 1; i <= n; ++ i) data[i] = a[i];
std::sort(data + 1, data + n + 1);
datanum = std::unique(data + 1, data + n + 1) - (data + 1);
for (int i = 1; i <= n; ++ i) {
a[i] = std::lower_bound(data + 1, data + datanum + 1, a[i]) - data;
}
for (int i = 1, l = 1; i <= datanum; ++ i) {
while (l <= i && data[l] + k < data[i]) ++ l;
b[i] = l;
}
for (int i = datanum, r = datanum; i; -- i) {
while (r >= i && data[r] > data[i] + k) -- r;
c[i] = r;
}
Seg::clear();
for (int i = n; i; -- i) {
next[i][0] = Seg::query(1, 1, datanum, b[a[i]], a[i]);
next[i][1] = Seg::query(1, 1, datanum, a[i], c[a[i]]);
Seg::modify(1, 1, datanum, a[i], i);
}
// for (int i = 1; i <= n; ++ i) {
// std::cout << next[i][0] << " " << next[i][1] << "\n";
// }
int q; std::cin >> q;
while (q --) {
int l, r, ans = 0; std::cin >> l >> r;
for (int i = l; i <= r; ++ i) f[i] = 1;
for (int i = l; i <= r; ++ i) {
ans = std::max(ans, f[i]);
for (int j = 0; j <= 1; ++ j) {
if (next[i][j] > r) continue;
f[next[i][j]] = std::max(f[next[i][j]], f[i] + 1);
}
}
std::cout << (r - l + 1) - ans << "\n";
}
}
return 0;
}
/*
1
6 9 2
1 1 4 5 1 4
1
1 6
*/
1008
DP,矩阵加速。
看到 \(n\le 100, L,R\le 10^{18}\),还给了 20S 的丧心病狂时限,一眼矩阵加速 DP。
1002
写在最后
学到了什么:
- 1007:根据转移的单调性,预处理最优决策。