CSP-S 2020 简要题解

补 CSP 上瘾了(无奈

题目链接

https://loj.ac/p?keyword=CSP-S%202020

题解

A. 儒略日 / julian

离线所有询问,按儒略日从小到大的顺序依次处理。对于公元 1582 年 10 月 4 日(含)之前的部分,直接暴力把所有日期扫一遍;对于公元 1582 年 10 月 15 日(含)以后的部分,先二分具体年份,再暴力按天扫到具体的月和日。

#include<bits/stdc++.h>

using namespace std;

const int N = 123456;
const int days[] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};

int n;
bool cont;

struct day_t {
  int d, m, y;
  bool bc;

  day_t() {
  }

  day_t(int d, int m, int y, bool bc): d(d), m(m), y(y), bc(bc) {
  }

  void print() {
    cout << d << ' ' << m << ' ' << y;
    if (bc) {
      cout << " BC";
    }
    cout << '\n';
  }
} answer[N];

int get_days(int m, int y, bool bc) {
  if ((bc && y % 4 == 1) || (!bc && y % 4 == 0)) {
    if (cont && !bc && (y % 4 || (y % 400 && y % 100 == 0))) {
      return days[m];
    } else {
      return m == 2 ? 29 : days[m];
    }
  } else {
    return days[m];
  }
}

void next_day(int& d, int& m, int& y, bool& bc) {
  ++d;
  if (d > get_days(m, y, bc)) {
    d = 1;
    if (++m == 13) {
      m = 1;
      if (bc) {
        if (--y == 0) {
          bc = false;
          ++y;
        }
      } else {
        ++y;
      }
    }
  }
}

long long check(int x) {
  long long result = (x - 1582) * 365ll;
  result += (x - 1580) / 4;
  result -= (x - 1500) / 100;
  result += (x - 1200) / 400;
  return result;
}

int main() {
  freopen("julian.in", "r", stdin);
  freopen("julian.out", "w", stdout);
  ios::sync_with_stdio(false);
  cin.tie(0);
  cin >> n;
  vector<pair<long long, int>> q;
  for (int i = 1; i <= n; ++i) {
    long long x;
    cin >> x;
    q.emplace_back(x, i);
  }
  sort(q.begin(), q.end());
  int p = 0, d = 1, m = 1, y = 4713, normal = 0;
  bool bc = true;
  while (1) {
    while (p < n && q[p].first == normal) {
      answer[q[p].second] = day_t(d, m, y, bc);
      ++p;
    }
    next_day(d, m, y, bc);
    if (d == 5 && m == 10 && y == 1582 && !bc) {
      break;
    }
    ++normal;
  }
  cont = true;
  while (p < n) {
    int l = 1582, r = (int)1e9;
    long long rest = q[p].first - normal - 1;
    while (l != r) {
      int mid = (l + r >> 1) + 1;
      if (rest >= check(mid)) {
        l = mid;
      } else {
        r = mid - 1;
      }
    }
    rest -= check(l);
    int d = 15, m = 10, y = l;
    bool bc = false;
    while (rest--) {
      next_day(d, m, y, bc);
    }
    answer[q[p].second] = day_t(d, m, y, bc);
    ++p;
  }
  for (int i = 1; i <= n; ++i) {
    answer[i].print();
  }
  return 0;
}

B. 动物园 / zoo

答案为 \(2^{k - x} - n\),其中 \(x\) 表示在 \(p_i\) 中出现过但又没有现有动物的编号在这一位为 \(1\) 的二进制位的数量。注意可能会出现 \(2^{64}\)

#include<bits/stdc++.h>

using namespace std;

int n, m, c, k;
bool exist[65];

int main() {
  freopen("zoo.in", "r", stdin);
  freopen("zoo.out", "w", stdout);
  ios::sync_with_stdio(false);
  cin.tie(0);
  cin >> n >> m >> c >> k;
  for (int i = 1; i <= n; ++i) {
    unsigned long long x;
    cin >> x;
    for (int j = 0; j < k; ++j) {
      if (x >> j & 1) {
        exist[j] = true;
      }
    }
  }
  int kinds = k;
  for (int i = 1; i <= m; ++i) {
    int x, y;
    cin >> x >> y;
    if (!exist[x]) {
      exist[x] = true;
      --kinds;
    }
  }
  if (kinds == 64) {
    if (!n) {
      cout << "18446744073709551616" << '\n';
    } else {
      cout << 18446744073709551615ull - (n - 1) << '\n';
    }
  } else {
    cout << (1ull << kinds) - n << '\n';
  }
  return 0;
}

C. 函数调用 / call

若将函数 \(x\) 调用函数 \(y\) 视为连了一条从 \(x\)\(y\) 的有向边,那么所有函数的调用关系构成了一个 DAG。为了方便,我们再新建一个函数节点 \(p\),并从 \(p\) 依次向所有要调用的函数连边,那么最终我们只需调用一次函数 \(p\) 即可。

对于数据 \(i\),其最终结果可以表示为 \(a_i \times k + b_i\),其中 \(k\) 表示所有调用过的类型 2 函数的乘数的积。调用每一个函数最终会使所有数据乘以多少可以通过 DAG 上的一遍 dfs 求出。问题在于如何求 \(b_i\)

假设某一时刻调用了一个类型 1 函数,让某个数据增加了 \(v_1\),之后又调用了一个类型 2 函数,让所有数据都乘以 \(v_2\),那么这实际上相当于在最开始调用类型 1 函数时让相应数据增加了 \(v_1 \times v_2\)。于是对于任一函数 \(x\),我们可以预处理出 \(x\) 在调用函数 \(y\)\(x\)\(y\) 间有连边)后还会调用到的所有函数的乘数的积(这样在调用 \(y\) 时让数据增加的数都会乘上这一积)。由于一个类型 1 函数可能被多个函数调用到,因此我们还需要求出所有对该类型 1 函数有影响的积的和,这可以通过在 DAG 的反图(将原 DAG 上的所有边反向)上进行 dfs 得到。

需要注意的是,两个函数间的连边可能不止一条(即一个函数可能重复调用另一个函数)。

#include<bits/stdc++.h>

using namespace std;

const int N = 123456;
const int mod = 998244353;

void add(int& x, int y) {
  x += y;
  if (x >= mod) {
    x -= mod;
  }
}

int mul(int x, int y) {
  return (long long) x * y % mod;
}

int n, m, q, a[N], b[N], type[N], p[N], value[N], mult[N], fun[N];
bool visit[N];
vector<int> adj[N];
vector<pair<int, int>> radj[N];

void dfs(int x) {
  visit[x] = true;
  if (type[x] == 2) {
    mult[x] = value[x];
  } else {
    mult[x] = 1;
    for (auto y : adj[x]) {
      if (!visit[y]) {
        dfs(y);
      }
      mult[x] = mul(mult[x], mult[y]);
    }
    reverse(adj[x].begin(), adj[x].end());
    int foobar = 1;
    for (auto y : adj[x]) {
      radj[y].emplace_back(x, foobar);
      foobar = mul(foobar, mult[y]);
    }
    reverse(adj[x].begin(), adj[x].end());
  }
}

void rdfs(int x) {
  visit[x] = true;
  for (auto e : radj[x]) {
    int y = e.first;
    if (!visit[y]) {
      rdfs(y);
    }
    add(mult[x], mul(mult[y], e.second));
  }
  if (type[x] == 1) {
    add(b[p[x]], mul(mult[x], value[x]));
  }
}

int main() {
  freopen("call.in", "r", stdin);
  freopen("call.out", "w", stdout);
  ios::sync_with_stdio(false);
  cin.tie(0);
  cin >> n;
  for (int i = 1; i <= n; ++i) {
    cin >> a[i];
  }
  cin >> m;
  for (int i = 1; i <= m; ++i) {
    cin >> type[i];
    if (type[i] == 1) {
      cin >> p[i] >> value[i];
    } else if (type[i] == 2) {
      cin >> value[i];
    } else {
      int k, x;
      cin >> k;
      while (k--) {
        cin >> x;
        adj[i].push_back(x);
      }
    }
  }
  cin >> q;
  for (int i = 1; i <= q; ++i) {
    cin >> fun[i];
  }
  ++m;
  for (int i = 1; i <= q; ++i) {
    adj[m].push_back(fun[i]);
  }
  dfs(m);
  int foobar = mult[m];
  memset(visit, false, sizeof visit);
  memset(mult, 0, sizeof mult);
  mult[m] = 1;
  for (int i = 1; i <= m; ++i) {
    if (type[i] == 1) {
      rdfs(i);
    }
  }
  for (int i = 1; i <= n; ++i) {
    cout << ((mul(a[i], foobar) + b[i]) % mod) << " \n"[i == n];
  }
  return 0;
}

D. 贪吃蛇 / snakes

如果决斗直到只剩一条蛇时才停止,那么每一轮决斗中哪条蛇吃掉了哪条蛇是确定的。某一轮决斗中最强的蛇选择让决斗在此时结束,必然是因为在下一轮被结束的决斗到来之前,它就已经被吃掉。假设我们模拟一遍所有决斗,求出了每轮决斗中最强的蛇的编号以及每条蛇在第几轮决斗中被吃掉,那么倒序扫一遍所有决斗即可求出哪些轮决斗是应被结束的。答案就来自于最早被结束的那一轮决斗。

考虑如何模拟所有决斗。此时,需要实现的过程可以形式化为:给定一个数集,每次取出集合中最大和最小的数,将两数的差重新放回集合,直至集合中只剩下一个数。直接用 set 模拟这一过程的总时间复杂度为 \(O(Tn \log n)\),需要使用双队列法优化至 \(O(Tn)\)

  • 建立队列 \(Q_1, Q_2\),初始时队列 \(Q_1\) 里由首到尾按顺序存有单调不降的 \(n\) 个数。每次将 \(Q_1\)\(Q_2\) 队尾(首)的数中较大(小)的取出作为所有数中最大(小)的数,两者作差得到需要插入队列的新数 \(x\),若 \(x\) 不大于 \(Q_1\) 中最小的数,则将 \(x\) 插入到 \(Q_1\) 的队首,否则将 \(x\) 插入到 \(Q_2\) 的队首。

上述做法中,由于队列中的数字需要随时保证有序性,因此插入 \(Q_2\) 的数满足单调不增。对此的简要证明:

将所有决斗分为两个阶段。假设一轮决斗中取出的最小数与最大数分别为 \(a, b\)
阶段一:\(b \geq 2a\),此时 \(b - a \geq a\),新数可能被插入到 \(Q_1\) 的队首,也可能被插入到 \(Q_2\) 的队首。注意到此时取出 \(a, b\),插入 \(b - a\) 后,所有数中最大的数不会比 \(b\) 大,最小的数不会比 \(a\) 小,因此下一轮取出的最大数与最小数的差不会大于 \(b - a\)
如果某一轮决斗出现 \(b < 2a\),则进入阶段二。
阶段二:该阶段的第一轮满足 \(b < 2a\),此时 \(b - a < a\),即该轮得到的新数为最小数,因此新数会被插入到 \(Q_1\) 的队首。
我们设该阶段开始时共有 \(m\) 个数,分别为 \(c_1 \sim c_m\),且满足 \(c_m \geq c_{m - 1} \geq \cdots \geq c_1\)(其中 \(c_1\) 即为 \(a\)\(c_m\) 即为 \(b\)),那么取出一次 \(a, b\),插入回 \(b - a\),我们有 \(c_{m - 1} \geq c_{m - 2} \geq \cdots > c_m - c_1\)。基于这两个不等式,运用归纳法,我们能证明对于任意小于 \(m - 1\) 的奇数 \(k\),不等式 \(c_{m - k - 1} \geq c_{m - k - 2} \geq \cdots \geq c_{m - k} - c_{m - k + 1} + c_{m - k + 2} - \cdots + (-1)^{k + 1}c_1\) 成立;对于任意小于 \(m - 1\) 的偶数 \(k\),不等式 \(c_{m - k - 1} \geq c_{m - k - 2} \geq \cdots > c_{m - k} - c_{m - k + 1} + c_{m - k + 2} - \cdots + (-1)^{k + 1}c_1\) 成立。即该阶段任意一轮得到的新数均为最小数,应被插入到 \(Q_1\) 的队首。
由于在阶段一中,每一轮取出的最大数与最小数的差一定不会大于前一轮的差,因此被插入 \(Q_2\) 的数是单调不增的。

不过在本题中,蛇的强弱还可能与编号有关,因此不能简单地将每条蛇看成一个数。但上述做法避开了编号的影响,证明如下:

在阶段一中,假设两次取出的蛇的体力的差值相同,由于后一次取出的最强蛇的体力不会大于前一次,最弱蛇的体力不会小于前一次,因此两次取出的最强蛇的体力必然相同。由于最强蛇(的编号)先从队列中取出就会被先放入队列,因此体力相同的蛇的编号的相对大小不会改变。最开始蛇的强弱顺序与编号大小顺序对应,故该情况下我们可以认为编号不会影响蛇的强弱,即 \(Q_2\) 内蛇的顺序始终按体力、编号双关键字递增。
根据对阶段一的分析,在阶段二开始的第一轮决斗中,编号的影响依然不会体现,但第二轮决斗完成后就可能出现最强蛇在决斗后体力最小但编号却更大的情况。假设在第一轮决斗中编号为 \(p\) 的蛇吃掉了编号为 \(q\) 的蛇,在第二轮中编号为 \(r\) 的蛇吃掉了编号为 \(p\) 的蛇。如果第二轮决斗不被结束,那么在第一轮中蛇 \(p\) 就会选择结束决斗(不然蛇 \(p\) 自己会在第二轮中被吃掉);否则决斗会在第二轮被结束。因此,阶段二内无论如何决斗都不可能持续到第二轮完成。换言之,即使从第二轮决斗完成时开始,队列中的编号顺序可能出现错误,最终答案也不会受到影响。

#include<bits/stdc++.h>

using namespace std;

const int N = 1234567;

class my_array {
  pair<int, int> a[N << 1];

 public:
  void reset() {
    memset(a, 0, sizeof a);
  }

  pair<int, int>& operator [] (int x) {
    return a[N + x];
  }
} b;

int n, a[N], p[N], foobar[N];

int main() {
  freopen("snakes.in", "r", stdin);
  freopen("snakes.out", "w", stdout);
  ios::sync_with_stdio(false);
  cin.tie(0);
  int tt;
  cin >> tt >> n;
  for (int i = 1; i <= n; ++i) {
    cin >> a[i];
  }
  auto solve = [&] () {
    int l = 1, r = n;
    b.reset();
    for (int i = 1; i <= n; ++i) {
      b[i] = make_pair(a[i], i);
      p[i] = n;
    }
    deque<pair<int, int>> q;
    for (int i = 1; i < n; ++i) {
      pair<int, int> out, eater;
      if (l <= r) {
        out = b[l];
        eater = b[r];
        ++l;
        --r;
        if (q.size()) {
          if (out > q.front()) {
            --l;
            out = q.front();
            q.pop_front();
          }
          if (eater < q.back()) {
            ++r;
            eater = q.back();
            q.pop_back();
          }
        }
      } else {
        out = q.front();
        eater = q.back();
        q.pop_front();
        q.pop_back();
      }
      foobar[i] = eater.second;
      p[out.second] = i;
      pair<int, int> new_snake = make_pair(eater.first - out.first, eater.second);
      if (l > r || new_snake < b[l]) {
        b[--l] = new_snake;
      } else {
        q.push_front(new_snake);
      }
    }
    int ban = n, answer = 1;
    for (int i = n - 1; i; --i) {
      if (p[foobar[i]] < ban) {
        answer = n - i + 1;
        ban = i;
      }
    }
    return answer;
  };
  cout << solve() << '\n';
  while (--tt) {
    int k, x, y;
    cin >> k;
    for (int i = 1; i <= k; ++i) {
      cin >> x >> y;
      a[x] = y;
    }
    cout << solve() << '\n';
  }
  return 0;
}

真的是……简……简要……题解?

posted @ 2021-08-13 20:45  ImagineC  阅读(337)  评论(0编辑  收藏  举报