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\) 取最小值时,显然最优的情况是:

\[2n = \frac{\operatorname{lcm}(x, y)}{x} = \frac{y}{\gcd(x, y)} \]

于是考虑判断 \(\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\),则有转移:

\[f_{i} = 1 + \max_{l\le j<i,\ a_i - k\le a_j \le a_i + k} f_{j} \]

对于上述状态转移方程,发现有贡献的位置的 \(a_j\) 是一段连续的权值区间,一个非常套路的做法是使用权值线段树进行维护,权值线段树叶节点 \([i, i]\) 维护结尾为权值 \(i\) 的子序列的最大长度,并维护区间最大值,转移时直接查询对应区间最大值即可。单次转移时间复杂度为 \(O(\log n)\) 级别,则总时间复杂度为 \(O(Tqn\log n)\) 级别,被卡了过不去呃呃呃呃

于是显然不能转移的时候用数据结构直接维护,于是考虑能否预处理最优决策,即可每次 \(O(1)\) 地转移。考虑换成刷表法从前往后地进行转移,则上述状态转移方程可写成:

\[f_{i} + 1 \rightarrow f_{j}(j > i,\ a_j - k\le a_i \le a_j + k) \]

发现每次从 \(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:根据转移的单调性,预处理最优决策。
posted @ 2024-08-10 15:02  Luckyblock  阅读(221)  评论(0编辑  收藏  举报