PA 2019 题解(20/22)

从今天起,关心粮食和蔬菜。

A + B

发现这种计算方式不存在进位,计算过程中只有每个相加的位在得数中对应一个长度为 1 或 2 的段,对原字符串进行每个划分的方案数 dp 即可。 Θ ( log n ) \Theta(\log n)

代码

Muzyka pop

考虑最高位的贡献, f ( k , l , r ) f(k, l, r) 表示规划 l r l\sim r 这一段单独规划, b b 的值都在 [ 0 , 2 k ) [0, 2^k) 内的方案,注意到最高位的贡献恰好是一个后缀,因此有 f ( k , l , r ) = max j { f ( k 1 , l , j 1 ) + f ( k 1 , j , r ) + s r s j 1 } f(k, l, r) = \max_j \{ f(k - 1, l, j - 1) + f(k - 1, j, r) + s_r - s_{j - 1} \} ,时间复杂度 Θ ( n 3 log m ) \Theta(n^3 \log m) 。那么处理出 f f 之后就可以用来类似的方程做数位 DP 了,这一部分只用对一个后缀来 DP。
时间复杂度 Θ ( n 3 log m ) \Theta(n^3 \log m)
代码

Wina

由于仅仅是最小化目标,考虑一个数能否被取到,用于更新答案即可。 Θ ( n 2 ) \Theta(n^2)
代码

Desant

考虑对 DP 进行状态化简,已经删去的数的区间我们只需记住其中一共选了几个数,这样 DP 的状态在考虑前 k k 个数时只有 i = 0 k ( 1 + b i ) \prod_{i=0}^k (1 + b_i) 个,而 i = 0 k b i = n k \sum_{i=0}^k b_i = n - k ,因此前面的乘积 ( n + 1 k + 1 ) k + 1 \leq \left(\frac{n + 1}{k+1}\right)^{k+1} ,复杂度 Θ ( k = 1 n ( k + 1 ) ( n + 1 k + 1 ) k + 1 ) \Theta\left(\sum_{k=1}^n (k+1)\left(\frac{n + 1}{k+1}\right)^{k+1}\right) ,这个求和的结果其实并不大。
代码

Herbata

首先至少要保证总共热量相同,也就是 l i a i = l i b i \sum l_i a_i = \sum l_i b_i

接着你猜测最值变成一个子区间就行,然后交了一发发现获得了 0 分的好成绩

考虑所有 ( l i , a i l i ) (l_i, a_i l_i) 向量按照斜率排序首尾相连组成的凸壳,发现任何一次平均操作本质上是让凸壳发生了收缩,而操作可以任意小,因此充要条件就是原状态的凸壳将终态的凸壳包住,对斜率排序即可。 Θ ( n log n ) \Theta(n\log n)

代码

Iloczyny Fibonacciego

注意到 F n + m = F n 1 F m 1 + F n F m F_{n+m} = F_{n-1}F_{m-1} + F_nF_m ,不难得到 F n F m = F n + m F n + m 2 + F n + m 4 + ( 1 ) min ( n , m ) F n m F_nF_m = F_{n+m} - F_{n+m-2} +F_{n+m-4} - \cdots + (-1)^{\min(n,m)} F_{|n-m|} ,对于这一非正规表示方法,我们不难先用卷积 A ( x ) × B ( x ) A(x)\times B(x) 得到前缀和在 n + m n + m 位置的修改,然后用 A ( x ) × B ( 1 / x ) x m A(-x)\times B(1/x) x^m 得到前缀和在 n m 2 |n - m| - 2 位置的修改。(注意这里在 i < j i < j i > j i > j 的时候差一个 ( 1 ) i j (-1)^{i-j} 的符号)
接下来我们得到了一个每个位置的系数在 Θ ( n 2 ) \Theta(n^2) 内的有正有负的表示,我们考虑通过二进制分解的方法可以 Θ ( n log x ) \Theta(n\log x) 将一组正的表示转成正规的表示,于是把正负分别转化,之后正规表示的减法是容易的。
时间复杂度 Θ ( n log n ) \Theta(n\log n)
代码

Terytoria

首先需注意到两个维度是独立的,接下来我们要做的就是分别计算。考虑一个维度上给出了 n n 个区间,我们每个区间选择该区间本身或者补集,最大化其交的大小。
注意到最后能取出的集合是两两不交的,考虑对于单位 [ i , i + 1 ] [i, i + 1] ,如果 i , j i, j 两个单位出现于同一个交集中当且仅当覆盖它们的区间集合相同。可以数据结构解决,但我不想写线段树分裂。考虑哈希,给每一个区间赋予一个随机数,将区间内的单位异或上这个数,最后取值相等的视为同一交集即可。由于要离散化所以需要排序, Θ ( n log n ) \Theta(n\log n) 。通过生日悖论粗略估算正确率:

k = 0 2 n 1 ( 1 2 64 k ) 2 e n ( 2 n 1 ) 2 63 1 n ( 2 n 1 ) 2 63 1 5.42 × 1 0 8 \begin{aligned} \prod_{k=0}^{2n-1} (1 - 2^{-64}k)^2 & \approx \mathrm{e}^{-n(2n-1)2^{-63}} \\ & \approx 1 - n(2n-1)2^{-63}\\ & \approx 1 - 5.42\times 10^{-8} \end{aligned}

代码

Szprotki i szczupaki

显然每次吃比自己小的鱼中最重的那条。假设下一条体重 \ge 自己的鱼为 w w ,那么我们每次假设在当前还能吃的鱼中进行线段树上二分,可以知道至少吃多少条才能使得体重达到 min ( k , w 1 ) \min(k, w - 1) 。注意到每次进行两轮二分体重至少倍增,所以只会进行 Θ ( log w ) \Theta(\log w) 轮。“吃鱼”可以通过在线段树上打一个永久化的标记完成,最后撤回所有吃鱼标记即可。
时间复杂度 Θ ( q log n log w ) \Theta(q\log n\log w)
代码

Wyspa

Trzy kule

考虑至少某个满足的方案数 2 n 2^n 减去全都不满足的方案数,后者等于 ( r 1 , r 2 , r 3 ) = ( n 1 r 1 , n 1 r 2 , n 1 r 3 ) (r'_1,r'_2,r'_3)=(n-1-r_1, n-1-r_2, n-1-r_3) 对应的答案。字符串本质上只有 4 4 种位置: 000 , 001 , 010 , 011 000,001,010,011 ,因为第一位可以异或掉后面的。考虑 Meet in the Middle,若枚举 000 , 011 000,011 中分别分配几个 0 , 1 0,1 ,那么这一部分造成距离贡献为 ( k 0 + k 3 , k 0 + c 3 k 3 , k 0 + c 3 k 3 ) (k_0 + k_3, k_0 + c_3 - k_3, k_0 + c_3 - k_3) 。造成的距离贡献只有两个自由度,将另外一部分拼凑的看做是点,这一部分看做是询问,这个三维数点的询问只有两个自由度且可以直接表示为 ( x , y , z ) + ( i + j , c 3 + i j , c 3 + i j ) ( r 1 , r 2 , r 3 ) (x,y,z)+(i+j, c_3 + i-j, c_3 + i-j)\le (r'_1,r'_2,r'_3) ,因此 max ( y r 2 , z r 3 ) c 3 + i j \max(y-r'_2,z-r'_3)\le c_3+i-j 即可。可以缩去一维,直接通过二维前缀和处理。时间复杂度 Θ ( n 2 ) \Theta(n^2)
代码

Osady i warownie 2

转为对偶图的连通性问题,但是并不是无向边。我们维护 S 可达区域和可达 T 的区域,S 的区域是一个右上角的阶梯形,T 的区域是左下角的一个阶梯形,加一条边的时候如果恰好切断,说明一端在 S 中一端在 T 中。用树状数组可以在加入一条边的时候扩展边界线,每条线维护一个堆用于找到扩展时新的可以可用边。
时间复杂度 Θ ( k log k ) \Theta(k\log k)
代码

Podatki drogowe

值域非常大,考虑随机二分。我们首先边分治,然后考虑每个 dist 值用一个可持久化线段树来存储,这样用 hash 就可以完成在 Θ ( log n ) \Theta(\log n) 时间内的 cmp。接下来就可以通过双指针在 Θ ( n log 2 n ) \Theta(n\log^2 n) 时间内确认一个数的 equal_range,总复杂度就是 Θ ( n log 3 n ) \Theta(n\log^3 n)
代码

Sonda

接下来的部分是 PA 2019 Final,由于并不公开数据,在 LOJ 上是没有的。由于是波兰语,本文也做简要题意叙述。

Zdjęcie rodzinne

简要题意:给一颗有根树,问找到一个最长的不重复点序列使得相邻两个点有祖先关系。 n 3 × 1 0 5 n\le 3\times 10^5

考虑一个子树的 dp f ( u , j ) f(u, j) 表示一个允许使用额外的子树外的 j j 个祖先的情况下,能够得到的最长序列。不难得到 dp 过程大致上是各孩子的一个 max 卷积。注意到这个过程显然关于 j j 始终是凸的,因此我们可以直接维护差分值,这个东西的实际意义是多加一个祖先能多合并的部分。因此算法就是:每个子树的 dp 值用一个堆表示。将各子树的堆合并,然后将最大的两个元素求和再 + 1 +1 再放回堆中(不够补 0 0 ),就得到了本节点的信息。而最终的答案是根节点的堆的最大值。
取决于使用启发式合并还是可并堆,复杂度为 Θ ( n log n ) Θ ( n log 2 n ) \Theta(n\log n) \sim \Theta(n\log^2 n)

#include <bits/stdc++.h>

using namespace std;

typedef long long ll;
typedef unsigned long long ull;

const int N = 300010;

int n;
vector<int> g[N];

priority_queue<int> q[N];

void dfs(int u) {
  for (int v : g[u]) {
    dfs(v);
    if (q[u].size() < q[v].size()) swap(q[u], q[v]);
    while (!q[v].empty()) {
      int x = q[v].top();
      q[v].pop();
      q[u].push(x);
    }
  }
  int x = 1;
  for (int rep = 0; rep < 2 && !q[u].empty(); ++rep) {
    x += q[u].top();
    q[u].pop();
  }
  q[u].push(x);
}

int main() {
#ifdef LBT
  freopen("test.in", "r", stdin);
  int nol_cl = clock();
#endif
  ios::sync_with_stdio(false);
  cin.tie(nullptr);

  cin >> n;
  for (int i = 2; i <= n; ++i) {
    int p;
    cin >> p;
    g[p].push_back(i);
  }
  dfs(1);
  printf("%d\n", q[1].top());

#ifdef LBT
  LOG("Time: %dms\n", int ((clock()
          -nol_cl) / (double)CLOCKS_PER_SEC * 1000));
#endif
  return 0;
}

Terytoria 2

简要题意:给出 n n 种动物,每种动物有 c c 个个体以及一个对应的禁忌矩形,你要把该种动物放在矩形之外,求最大化全体动物处在相同位置的动物对数。 n 1 0 5 , X , Y 1 0 3 n\le 10^5, X, Y\le 10^3

考虑最优解的性质,每次我们找到那个最优解里面人最多的格子,那么显然其他任何动物都不能移动到这个格子,否则答案变大。因此我们只需要考虑一个枚举顺序,每次让这个格子里的人尽量多放。注意到我们首先确定第一个放置点后,没有放置的就是包含这个矩形的所有种族,接下来考虑一个格子里如果放了数,那么它一定可以让这些种族全部被挪到一个角上,所以我们下一步一定是枚举一个角,接下来剩下的一定都可以放在对角上。这个信息用前缀和就可以处理了。时间复杂度 Θ ( n + X Y ) \Theta(n + XY)

另外这也证明了最优解中,全体动物最多放在三个位置。

#include <bits/stdc++.h>

#define LOG(FMT...) fprintf(stderr, FMT)

using namespace std;

typedef long long ll;
typedef unsigned long long ull;

const int X = 1010;

int s[17][X][X];

void add(int k, int x1, int y1, int x2, int y2, int v) {
  s[k][x1][y1] += v;
  s[k][x1][y2 + 1] -= v;
  s[k][x2 + 1][y1] -= v;
  s[k][x2 + 1][y2 + 1] += v;
}

ll gval(int x) { return x * (x - 1LL) / 2; }

int main() {
#ifdef LBT
  freopen("test.in", "r", stdin);
  int nol_cl = clock();
#endif
  ios::sync_with_stdio(false);
  cin.tie(nullptr);

  int n, x, y, tot = 0;
  cin >> n >> x >> y;
  while (n--) {
    int x1, y1, x2, y2, c;
    cin >> x1 >> y1 >> x2 >> y2 >> c;
    int v = 0;
    tot += c;
    v |= (x1 == 1 && y1 == 1);
    v |= (x1 == 1 && y2 == y) << 1;
    v |= (x2 == x && y1 == 1) << 2;
    v |= (x2 == x && y2 == y) << 3;
    add(16, 1, 1, x, y, c);
    add(16, x1, y1, x2, y2, -c);
    add(v, x1, y1, x2, y2, c);
  }
  for (int k = 0; k <= 16; ++k)
    for (int i = 1; i <= x; ++i)
      for (int j = 1; j <= y; ++j)
        s[k][i][j] += s[k][i - 1][j] + s[k][i][j - 1] - s[k][i - 1][j - 1];
  ll ans = 0;
  for (int i = 1; i <= x; ++i)
    for (int j = 1; j <= y; ++j) {
      ll cur = gval(s[16][i][j]);
      for (int k = 0; k < 4; ++k) {
        int cs = 0;
        for (int t = 0; t < 16; ++t)
          if (!((t >> k) & 1))
            cs += s[t][i][j];
        ans = max(ans, cur + gval(cs) + gval(tot - s[16][i][j] - cs));
      }
    }
  cout << ans << '\n';

#ifdef LBT
  LOG("Time: %dms\n", int ((clock()
          -nol_cl) / (double)CLOCKS_PER_SEC * 1000));
#endif
  return 0;
}

Pionki

简要题意:给一个三维棋盘,每个格子上有一些棋子,可以将棋子沿 x , y , z x,y,z 正方向移动,问状态 a a 能不能变为状态 b b 。其中 B , C 6 B,C\le 6 。多组询问 T = 1 0 4 T=10^4 ,其中 A 1 0 4 \sum A\le 10^4

考虑最大流转最小割,我们只需要按第一维的层状压转移即可,由于 S , T S,T 两部一定是一个当层的一个割的状态,所以状态数是 ( B + C B ) \binom{B+C}B ,注意转移之间也可以简化成一个个增补的过程,所以每个节点的转移是 < B + C <B+C 的。时间复杂度 Θ ( A ( B + C B ) ( B + C ) ) \Theta(A \binom{B+C}B (B+C))

多组数据初始化可能略慢,可以特判 A = 1 A=1 卡常数(这部分可以做到 Θ ( B C ) \Theta(BC) )。

#include <bits/stdc++.h>

#define LOG(FMT...) fprintf(stderr, FMT)

using namespace std;

typedef long long ll;
typedef unsigned long long ull;

struct TrieMap {

  int A;
  vector<vector<int>> trans;
  vector<ll> value;

  TrieMap(int A) : A(A), trans(1, vector<int>(A, -1)), value(1, -1) {}

  ll get(int o) const { return value[o]; }

  int getId(const vector<int>& s) const {
    int o = 0;
    for (int c : s) {
      o = trans[o][c];
      if (o == -1)
        return -1;
    }
    return o;
  }

  int extend(int o, int c) {
    if (trans[o][c] == -1) {
      trans[o][c] = trans.size();
      trans.push_back(vector<int>(A, -1));
      value.push_back(-1);
    }
    return trans[o][c];
  }

  void ins(const vector<int>& s, ll v) {
    int o = 0;
    for (int c : s) o = extend(o, c);
    value[o] = v;
  }
};

int main() {
#ifdef LBT
  freopen("test.in", "r", stdin);
  int nol_cl = clock();
#endif

  int T;
  scanf("%d", &T);
  while (T--) {
    int A, B, C;
    scanf("%d%d%d", &A, &B, &C);
    vector<vector<vector<ll>>> a(A, vector<vector<ll>>(B, vector<ll>(C))), b = a;
    ll totA = 0, totB = 0;
    for (int i = 0; i < A; ++i)
      for (int j = 0; j < B; ++j)
        for (int k = 0; k < C; ++k) {
          scanf("%lld", &a[i][j][k]);
          totA += a[i][j][k];
        }
    for (int i = 0; i < A; ++i)
      for (int j = 0; j < B; ++j)
        for (int k = 0; k < C; ++k) {
          scanf("%lld", &b[i][j][k]);
          totB += b[i][j][k];
        }
    if (totA != totB) {
      puts("NIE");
      continue;
    }
    if (A == 1) {
      bool flag = true;
      vector<ll> c(C);
      for (int i = 0; i < B; ++i) {
        ll need = 0;
        for (int j = C - 1; j >= 0; --j) {
          need += b[0][i][j];
          c[j] += a[0][i][j];
          ll v = min(need, c[j]);
          need -= v;
          c[j] -= v;
        }
        if (need != 0) {
          flag = false;
          break;
        }
      }
      puts(flag ? "TAK" : "NIE");
      continue;
    }
    TrieMap trieMap(C + 1);
    vector<int> stk;
    function<void(int, int, int, const function<void(int)>&)> dfs = [&](int i, int j, int o, const function<void(int)>& f) {
      if (i == B) {
        f(o);
        return;
      }
      for (; j >= 0; --j) {
        stk.push_back(j);
        dfs(i + 1, j, trieMap.extend(o, j), f);
        stk.pop_back();
      }
    };
    dfs(0, C, 0, [&](int o) {
      trieMap.value[o] = 0;
      trieMap.trans[o].assign(B, -1);
      for (int i = 0; i < B; ++i) {
        if (stk[i] < C) {
          ++stk[i];
          trieMap.trans[o][i] = trieMap.getId(stk);
          --stk[i];
        }
      }
    });
    for (int i = 0; i < A; ++i) {
      vector<vector<ll>> cost(B, vector<ll>(C + 1));
      ll tot = 0;
      for (int j = 0; j < B; ++j) {
        for (int k = 0; k < C; ++k) {
          tot += b[i][j][k];
          cost[j][k + 1] = cost[j][k] + a[i][j][k] - b[i][j][k];
        }
      }
      dfs(0, C, 0, [&](int o) {
        ll cur = trieMap.get(o) + tot;
        for (int i = 0; i < B; ++i)
          cur += cost[i][stk[i]];
        for (int i = 0; i < B; ++i)
          if (stk[i] < C) {
            ++stk[i];
            int p = trieMap.trans[o][i];
            if (p != -1)
              cur = min(cur, trieMap.get(p) - cost[i][stk[i]] + cost[i][stk[i] - 1]);
            --stk[i];
          }
        trieMap.ins(stk, cur);
      });
    }
    ll minCut = numeric_limits<ll>::max();
    dfs(0, C, 0, [&](int o) { minCut = min(minCut, trieMap.get(o)); });
    puts((minCut == totA) ? "TAK" : "NIE");
  }

#ifdef LBT
  LOG("Time: %dms\n", int ((clock()
          -nol_cl) / (double)CLOCKS_PER_SEC * 1000));
#endif
  return 0;
}

Parzysty deszcz

简要题意:给一个直方图,每个位置有个高度 h i h_i ,问有多少种方法将其中 k k 个位置高度抹成 0 0 ,之后剩余部分的极大积水面积是偶数。同余 1 0 9 + 7 10^9 + 7 n 25000 , k 25 n\le 25000, k\le 25

积水的高度就是两个方向看到的 max \max 的较小值,因此我们只需考虑这个结果的前缀 max \max 和前缀 min \min ,考虑一个数向后找到下一个比它大的数,我们显然只有 k + 1 k+1 种选择,也就是这么多种转移,这个可以排序后用一个 set 轻松找到。转移就是一个多项式乘法。总共时间复杂度 Θ ( n log n + n k 3 ) \Theta(n\log n + nk^3) 这个 OJ 真是卡常,这道题 9s 也卡

#include <bits/stdc++.h>

#define LOG(FMT...) fprintf(stderr, FMT)

using namespace std;

typedef long long ll;
typedef unsigned long long ull;

const int N = 25010, K = 26, P = 1000000007;
const ll P2 = P * (ll)P;

int h[N], cnt[N], fac[N], ifac[N];
int bin[N][K];
int conv[K][2];
int trans[N][K], rtrans[N][K];
int od[N][K], rod[N][K];
int dp[N][K][2], rdp[N][K][2];
pair<int, int> ord[N];

int mpow(int x, int k) {
  int ret = 1;
  while (k) {
    if (k & 1) ret = ret * (ll)x % P;
    k >>= 1;
    x = x * (ll)x % P;
  }
  return ret;
}

int norm(int x) { return x >= P ? x - P : x; }

int binom(int n, int m) { return (m < 0 || m > n) ? 0 : bin[n][m]; }

int sbinom(int n, int m) { return (m < 0 || m > n) ? 0 : ((m & 1) ? norm(P - bin[n][m]) : bin[n][m]); }

int main() {
#ifdef LBT
  freopen("test.in", "r", stdin);
  int nol_cl = clock();
#endif
  ios::sync_with_stdio(false);
  cin.tie(nullptr);

  int n, k;
  cin >> n >> k;
  for (int i = 1; i <= n; ++i) cin >> h[i];
  for (int i = 0; i <= n + 1; ++i) ord[i] = make_pair(h[i], i);
  sort(ord, ord + n + 2, greater<pair<int, int>>());
  set<int> s;
  for (int i = 0; i <= n + 1; ++i) {
    int p = ord[i].second;
    auto it = s.insert(p).first;
    auto jt = make_reverse_iterator(it); --jt;
    int odds = 0;
    for (int j = 0; j <= k; ++j) {
      if (++it == s.end()) {
        trans[p][j] = -1;
        break;
      }
      od[p][j] = odds;
      trans[p][j] = *it;
      odds += h[*it] & 1;
    }
    odds = 0;
    for (int j = 0; j <= k; ++j) {
      if (++jt == s.rend()) {
        rtrans[p][j] = -1;
        break;
      }
      rod[p][j] = odds;
      rtrans[p][j] = *jt;
      odds += h[*jt] & 1;
    }
  }
  for (int i = 1; i <= n + 1; ++i) cnt[i] = cnt[i - 1] + (h[i] & 1);
  for (int i = 0; i <= n; ++i) {
    bin[i][0] = 1;
    for (int j = 1; j <= min(i, k); ++j)
      bin[i][j] = norm(bin[i - 1][j - 1] + bin[i - 1][j]);
  }
  dp[0][0][0] = dp[0][0][1] = 1;
  rdp[n + 1][0][0] = rdp[n + 1][0][1] = 1;
  for (int i = 0; i < n; ++i) {
    for (int j = 0; j <= k; ++j) {
      if (trans[i][j] == -1) break;
      int p = trans[i][j];
      memset(conv, 0, sizeof(conv));
      bool par = (od[i][j] & 1) ^ ((h[i] & 1) & (p - 1 - i)) ^ ((cnt[p - 1] - cnt[i]) & 1);
      int r1 = cnt[p - 1] - cnt[i] - od[i][j], r0 = p - 1 - i - j - r1;
      for (int t = 0; t <= k - j; ++t) {
        conv[t][0] = binom(r0 + r1, t);
        ll v = 0;
        for (int u = 0; u <= t; ++u) {
          v += sbinom(r1, u) * (ll) binom(r0, t - u);
          if (v >= P2) v -= P2;
        }
        conv[t][1] = v % P;
        if (par) conv[t][1] = norm(P - conv[t][1]);
      }
      for (int x = j; x <= k; ++x)
        for (int b = 0; b <= 1; ++b) {
          ll ad = 0;
          for (int y = 0; y <= x - j; ++y) {
            ad += dp[i][x - y - j][b] * (ll) conv[y][b];
            if (ad >= P2)
              ad -= P2;
          }
          dp[p][x][b] = (dp[p][x][b] + ad) % P;
        }
    }
  }
  for (int i = n + 1; i >= 2; --i) {
    for (int j = 0; j <= k; ++j) {
      if (rtrans[i][j] == -1) break;
      int p = rtrans[i][j];
      memset(conv, 0, sizeof(conv));
      bool par = (rod[i][j] & 1) ^ ((h[i] & 1) & (i - 1 - p)) ^ ((cnt[i - 1] - cnt[p]) & 1);
      int r1 = cnt[i - 1] - cnt[p] - rod[i][j], r0 = i - 1 - p - j - r1;
      for (int t = 0; t <= k - j; ++t) {
        conv[t][0] = binom(r0 + r1, t);
        ll v = 0;
        for (int u = 0; u <= t; ++u) {
          v += sbinom(r1, u) * (ll) binom(r0, t - u);
          if (v >= P2) v -= P2;
        }
        conv[t][1] = v % P;
        if (par) conv[t][1] = norm(P - conv[t][1]);
      }
      for (int x = j; x <= k; ++x)
        for (int b = 0; b <= 1; ++b) {
          ll ad = 0;
          for (int y = 0; y <= x - j; ++y) {
            ad += rdp[i][x - y - j][b] * (ll) conv[y][b];
            if (ad >= P2)
              ad -= P2;
          }
          rdp[p][x][b] = (rdp[p][x][b] + ad) % P;
        }
    }
  }

  int ans = 0;
  for (int i = 1; i <= n; ++i)
    for (int j = 0; j <= k; ++j)
      for (int t = 0; t <= 1; ++t)
        ans = (ans + dp[i][j][t] * (ll) rdp[i][k - j][t]) % P;
  ans = ((ans & 1) ? (ans + P) : ans) >> 1;
  cout << ans << '\n';

#ifdef LBT
  LOG("Time: %dms\n", int ((clock()
          -nol_cl) / (double)CLOCKS_PER_SEC * 1000));
#endif
  return 0;
}

Zaczarowany Ołówek

简要题意:给出 n n 个三角形,依次按照给定的点的顺序绘制,当一个点被画过偶数次就会消失,奇数次就会重现,问结果得到的可见线段长度和。 n 1 0 5 n\le 10^5

容易发现这就是一个线段的异或和问题,每个直线上的各自排序即可。时间复杂度 Θ ( N log N ) \Theta(N\log N)

#include <bits/stdc++.h>

#define LOG(FMT...) fprintf(stderr, FMT)

using namespace std;

typedef long long ll;
typedef unsigned long long ull;

ll gcd(ll a, ll b) { return b ? gcd(b, a % b) : a; }

int main() {
#ifdef LBT
  freopen("test.in", "r", stdin);
  int nol_cl = clock();
#endif
  ios::sync_with_stdio(false);
  cin.tie(nullptr);

  int n;
  cin >> n;
  vector<pair<tuple<int, int, ll>, pair<int, int>>> points;
  function<void(int, int, int, int)> add = [&](int x1, int y1, int x2, int y2) {
    int A = y1 - y2, B = x2 - x1;
    ll C = x1 * (ll)(y2 - y1) - y1 * (ll)(x2 - x1);
    ll g = gcd(gcd(A, B), C);
    A /= g;
    B /= g;
    C /= g;
    if (A < 0) {
      A = -A;
      B = -B;
      C = -C;
    } else if (A == 0) {
      if (B < 0) {
        B = -B;
        C = -C;
      } else if (B == 0 && C < 0)
        C = -C;
    }
    points.emplace_back(make_tuple(A, B, C), make_pair(x1, y1));
    points.emplace_back(make_tuple(A, B, C), make_pair(x2, y2));
  };
  while (n--) {
    int x1, y1, x2, y2, x3, y3;
    cin >> x1 >> y1 >> x2 >> y2 >> x3 >> y3;
    add(x1, y1, x2, y2);
    add(x2, y2, x3, y3);
    add(x3, y3, x1, y1);
  }
  sort(points.begin(), points.end());
  double ans = 0;
  function<ll(int)> sq = [&](int x) { return x * (ll)x; };
  for (int i = 0; i < points.size(); i += 2) {
    if (points[i].first == points[i + 1].first) {
      ans += sqrt(sq(points[i].second.first - points[i + 1].second.first) +
                 sq(points[i].second.second - points[i + 1].second.second));
    }
  }
  cout.precision(10);
  cout << fixed << ans << '\n';

#ifdef LBT
  LOG("Time: %dms\n", int ((clock()
          -nol_cl) / (double)CLOCKS_PER_SEC * 1000));
#endif
  return 0;
}

Łamana 2

简要题意:一个字符串,问如果将每个相同字符替换成同方向的一个箭头(向上或向下),则最大的首尾相连构成这条路径的下方围着的面积是多少? S 3 × 1 0 5 , A = 16 |S|\le 3\times 10^5, |A| = 16

注意到这个面积等价于逆序对,我们可以在 Θ ( S A ) \Theta(|S|A) 内算出每对字符,前者当 1,后者当 0 产生的逆序对,最后 Θ ( A 2 2 A ) \Theta(|A|^22^{|A|}) 枚举一下就可以通过了。

#include <bits/stdc++.h>

#define LOG(FMT...) fprintf(stderr, FMT)

using namespace std;

typedef long long ll;
typedef unsigned long long ull;

int main() {
#ifdef LBT
  freopen("test.in", "r", stdin);
  int nol_cl = clock();
#endif
  ios::sync_with_stdio(false);
  cin.tie(nullptr);

  string str;
  cin >> str;
  vector<int> cnt(16);
  vector<vector<ll>> tot(16, vector<ll>(16));
  for (char c : str) {
    ++cnt[c - 'a'];
    for (int i = 0; i < 16; ++i)
      tot[i][c - 'a'] += cnt[i];
  }
  ll ans = 0;
  for (int s = 0; s < (1 << 16); ++s) {
    ll cur = 0;
    for (int i = 0; i < 16; ++i)
      for (int j = 0; j < 16; ++j)
        if (((s >> i) & 1) && !((s >> j) & 1))
          cur += tot[i][j];
    ans = max(ans, cur);
  }
  cout << ans << '\n';

#ifdef LBT
  LOG("Time: %dms\n", int ((clock()
          -nol_cl) / (double)CLOCKS_PER_SEC * 1000));
#endif
  return 0;
}

Grafy

简要题意:问有多少个 N N 个顶点有标号的无向简单图,每个点的入度出度都是 2 2 。答案对 P P 取模。 N 500 N\le 500

考虑容斥。我们首先看成一个 0 2 N 1 0 \sim 2N -1 的排列,然后每个节点要求就是 p 2 i / 2 p 2 i + 1 / 2 p 2 i / 2 i p 2 i + 1 / 2 i \lfloor p_{2i}/2\rfloor \neq \lfloor p_{2i+1}/2\rfloor \wedge \lfloor p_{2i}/2\rfloor \neq i \wedge \lfloor p_{2i+1}/2\rfloor \neq i 容斥信息就是 i = 0 N 1 ( 1 [ p 2 i / 2 = p 2 i + 1 / 2 ] [ p 2 i / 2 = i ] [ p 2 i + 1 / 2 = i ] + 2 [ p 2 i / 2 = i p 2 i + 1 / 2 = i ] ) \prod_{i=0}^{N-1} (1 - [\lfloor p_{2i} / 2 \rfloor = \lfloor p_{2i + 1} / 2 \rfloor] - [\lfloor p_{2i}/2\rfloor = i] - [\lfloor p_{2i+1}/2\rfloor = i] + 2[\lfloor p_{2i}/2\rfloor = i \wedge \lfloor p_{2i+1}/2\rfloor = i]) ,所以我们可以直接列出一个三变量容斥式子(因为其中两个单自环假设是同质的)
1 2 2 n i + j + k N 2 i ( 1 ) j ( 1 ) k ( N i , j , k , N i j k ) 2 i 2 2 j ( N i j k ) k ! 2 k ( 2 N 2 i j 2 k ) ! \frac1{2^{2n}} \sum_{i+j+k\le N} 2^i (-1)^j (-1)^k \binom N{i,j,k,N-i-j-k} 2^i 2^{2j} \binom{N-i-j}k k! 2^k (2N-2i-j-2k)!
这个 oj 有点卡常,我们稍微减小一下常数。化简一下就是
n ! 2 2 n i + j + k n ( 2 n 2 i j 2 k ) ! ( n i j ) ! i ! j ! k ! ( n i j k ) ! 2 ( 1 ) j + k 2 2 i + 2 j + k \frac{n!}{2^{2n}}\sum_{i+j+k\le n} \frac{(2n-2i-j-2k)!(n-i-j)!}{i!j!k!(n-i-j-k)!^2} (-1)^{j+k} 2^{2i+2j+k}
时间复杂度 Θ ( N 3 ) \Theta(N^3)

#include <bits/stdc++.h>

#define LOG(FMT...) fprintf(stderr, FMT)

using namespace std;

typedef long long ll;
typedef unsigned long long ull;

const int N = 1010;

int P;
int fac[N], ifac[N], pw2[N], sgn[N];

int norm(int x) { return x >= P ? x - P : x; }

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

int binom(int n, int m) { return fac[n] * (ll)ifac[m] % P * ifac[n - m] % P; }

int ifac4(int a, int b, int c, int d) { return ifac[a] * (ll)ifac[b] % P * ifac[c] % P * ifac[d] % P; }

int main() {
#ifdef LBT
  freopen("test.in", "r", stdin);
  int nol_cl = clock();
#endif
  ios::sync_with_stdio(false);
  cin.tie(nullptr);

  int n;
  cin >> n >> P;
  n *= 2;
  fac[0] = 1;
  for (int i = 1; i <= n; ++i) fac[i] = fac[i - 1] * (ll)i % P;
  ifac[1] = 1;
  for (int i = 2; i <= n; ++i) ifac[i] = -(P / i) * (ll)ifac[P % i] % P + P;
  ifac[0] = 1;
  for (int i = 1; i <= n; ++i) ifac[i] = ifac[i - 1] * (ll)ifac[i] % P;
  pw2[0] = 1;
  for (int i = 1; i <= n; ++i) pw2[i] = norm(pw2[i - 1] << 1);
  sgn[0] = 1;
  for (int i = 1; i <= n; ++i) sgn[i] = P - sgn[i - 1];

  n /= 2;
  int ans = 0;
  for (int i = 0; i <= n; ++i)
    for (int j = 0; j <= n - i; ++j)
      for (int k = 0; k <= n - i - j; ++k)
        add(ans, (ll)fac[n * 2 - i * 2 - j - k * 2] * ifac4(i, j, k, n - i - j - k) % P * pw2[i * 2 + j * 2 + k] % P * sgn[j + k] % P * fac[n - i - j] % P * ifac[n - i - j - k] % P);
  ans = ans * (ll)fac[n] % P;
  for (int rep = 0; rep < n * 2; ++rep) {
    if (ans & 1) ans = (ans + P) >> 1;
    else ans >>= 1;
  }
  cout << ans << '\n';

#ifdef LBT
  LOG("Time: %dms\n", int ((clock()
          -nol_cl) / (double)CLOCKS_PER_SEC * 1000));
#endif
  return 0;
}

Gdzie jest jedynka? 2

简要题意:交互题,一个 0 n 1 0\sim n-1 的排列,每次可以询问 gcd ( p i , p j ) ( i j ) \gcd(p_i, p_j) (i\neq j) ,在 2.5 n \lceil 2.5n \rceil 次询问内找出 1 1 所在的位置。多组数据, n 5 × 1 0 5 \sum n\le 5\times 10^5

首先我们假设 n > 3 n>3 ,我们首先考虑从一个数开始算它和每个数的 gcd \gcd ,如果是 1 1 那么得到的都是 1 1 ,否则我们可以知道它的倍数都是谁。但是还有一个例外就是它是 0 0 ,这样就会得到最大的数。我们考虑不停地从留下的数里面筛,直到剩下两个数。这个过程我们已经支付了最多 n + n k 1 + n k 1 k 2 + < 2 n n + \frac{n}{k_1} + \frac{n}{k_1k_2} + \cdots < 2n 次询问。如果我们知道了 0 0 ,那么从第一次筛的数剩下的里面找,总共支付 n + n + n k 1 k 2 + < 2.5 n n + n + \frac{n}{k_1k_2} + \cdots < 2.5n 次询问。但是问题是我们还要用次数来找到剩下两个数里谁是 0 0 。我们考虑从第一次筛掉的数里面找证据,如果两个数和同一个数的 gcd \gcd 不同,则说明这个证据不是那个大数的因子,而 gcd \gcd 较大的那个对应是 0 0 。在特判 1 1 的情况下我们之前检测的都不需要再来一遍,那么我们目前最坏情况就是在 k 1 ∤ v k_1 \not | v v v 中,较大数的因子正好在前面全部撞上。当靠前的有 k j 2 k_j \neq 2 时,由于之前的询问空出来一些,可以认为是远够 d ( n ) d(n) 用的。否则 k j k_j 前面全是 2 2 ,那么那个数在奇数里的因子就会非常少。
以上分析并不够具体,但是在实验后发现确实做到了操作次数在范围内。

#include <algorithm>
#include <numeric>
#include <vector>
#include <random>
#include <chrono>

#include "gdzlib.h"

using namespace std;

int main() {
  int N;
  while ((N = GetN()) != -1) {
    vector<int> cnt(N), candidate(N), level(N);
    vector<bool> exc(N);
    iota(candidate.begin(), candidate.end(), 0);
    int x = 1;
    bool flag = false;
    while (candidate.size() > 2) {
      vector<int> res(candidate.size());
      for (int j : candidate) ++level[j];
      for (int j = 1; j < candidate.size(); ++j) {
        ++cnt[res[j] = Ask(candidate[0], candidate[j])];
        if (res[j] != 1) {
          exc[candidate[0]] = exc[candidate[j]] = true;
        }
      }
      if (x == 1 && cnt[1] == N - 1) {
        flag = true;
        Answer(candidate[0]);
        break;
      }
      bool fl = true;
      for (int j = x; j < N; j += x) if (!cnt[j]) fl = false;
      if (fl && (x == 1 || candidate.size() > 3)) {
        flag = true;
        for (int j = 0; j < N; ++j)
          if (j != candidate[0] && !exc[j] && Ask(candidate[0], j) == 1) {
            Answer(j);
            break;
          }
        break;
      }
      int k = (N - 1) / x * x;
      while (!cnt[k]) k -= x;
      for (int j = 1; j < candidate.size(); ++j)
        --cnt[res[j]];
      vector<int> newCandidate;
      for (int j = 1; j < candidate.size(); ++j)
        if (res[j] == k)
          newCandidate.push_back(candidate[j]);
      newCandidate.push_back(candidate[0]);
      if (newCandidate.size() == 2 && candidate.size() > 3) {
        int pos0 = newCandidate[0];
        flag = true;
        for (int i = 0; i < N; ++i)
          if (!exc[i]) {
            if (Ask(pos0, i) == 1) {
              Answer(i);
              break;
            }
          }
        break;
      }
      swap(candidate, newCandidate);
      x = k;
    }
    if (flag) continue;
    int pos0 = -1;
    vector<pair<int, int>> val(N);
    for (int j = 0; j < N; ++j) val[j] = make_pair(level[j], j);
    sort(val.begin(), val.end());
    for (int j = 0; j < N; ++j) {
      int iv = val[j].second;
      if (iv == candidate[0] || iv == candidate[1]) continue;
      exc[iv] = true;
      int a = Ask(iv, candidate[0]), b = Ask(iv, candidate[1]);
      if (a == 1 && b == 1) {
        Answer(iv);
        break;
      }
      if (a == b) continue;
      pos0 = (a < b) ? candidate[1] : candidate[0];
      for (int i = 0; i < N; ++i)
        if (!exc[i]) {
          if (Ask(pos0, i) == 1) {
            Answer(i);
            break;
          }
        }
      break;
    }
  }
  return 0;
}

Floyd-Warshall

简要题意:给一个有向有权图,问使用如下循环方式计算的最短路有多少对是错误的:

for (i : [1, n])
  for (j : [1, n])
    for (k : [1, n])
      G[i][j] = min(G[i][j], G[i][k] + G[k][j])

其中 n 2 × 1 0 3 , m 3 × 1 0 3 n\le 2\times 10^3, m\le 3\times 10^3

首先我们跑出所有点对最短路是可以 Θ ( n m log m ) \Theta(nm\log m) ,这是可接受的。然后考虑这个循环的本质就是给全体点对按顺序尝试计算,那么我们考虑一个点对是正确的当且仅当:

  1. 要么 u , v u,v 没有路径
  2. 要么 u , v u,v 之间有直接连边,且就是最短的
  3. 在处理 u , v u, v 之前,存在一个点 w w ,使得 d ( u , v ) = d ( u , w ) + d ( w , v ) d(u,v)=d(u,w)+d(w,v) ,且 u , w u,w w , v w,v 间的最短路已经正确计算出了。

所有满足等式的 w w 可以在最短路 DAG 上传递闭包算出。那么我们只需要按顺序额外维护每个点到哪些点的距离,以及哪些点到这个点是被正确计算的。用 bitset 加速。时间复杂度 Θ ( n m log m + m n 2 w ) \Theta(nm\log m + \frac{mn^2}{w})

#include <bits/stdc++.h>

#define LOG(FMT...) fprintf(stderr, FMT)

using namespace std;

struct Node {
  int u, step;

  Node(int u, int step) : u(u), step(step) {}

  bool operator>(const Node& rhs) const { return step > rhs.step; }
};

const int N = 2010;

int n, m;
vector<pair<int, int>> g[N];
bitset<N> in[N], out[N], clo[N];
int dis[N], mat[N][N];

int main() {
#ifdef LBT
  freopen("test.in", "r", stdin);
  int nol_cl = clock();
#endif
  ios::sync_with_stdio(false);
  cin.tie(nullptr);

  cin >> n >> m;
  while (m--) {
    int u, v, w;
    cin >> u >> v >> w;
    g[u].emplace_back(v, w);
  }
  for (int i = 1; i <= n; ++i) {
    memset(dis, -1, sizeof(dis));
    dis[i] = 0;
    priority_queue<Node, vector<Node>, greater<Node>> q;
    q.emplace(i, 0);
    while (!q.empty()) {
      Node tmp = q.top(); q.pop();
      if (dis[tmp.u] != tmp.step) continue;
      for (const auto& [v, w] : g[tmp.u])
        if (dis[v] == -1 || dis[v] > tmp.step + w)
          q.emplace(v, dis[v] = tmp.step + w);
    }
    copy(dis + 1, dis + n + 1, mat[i] + 1);
  }
  for (int i = 1; i <= n; ++i) {
    in[i].set(i);
    out[i].set(i);
    for (const auto& [j, w]: g[i])
      if (mat[i][j] == w) {
        in[i].set(j);
        out[j].set(i);
      }
  }
  int ans = 0;
  for (int i = 1; i <= n; ++i) {
    for (int j = 1; j <= n; ++j) {
      clo[j].reset();
      clo[j].set(j);
    }
    vector<pair<int, int>> du;
    for (int j = 1; j <= n; ++j)
      du.emplace_back(mat[i][j], j);
    sort(du.begin(), du.end());
    for (const auto& [d, u] : du)
      for (const auto& [v, w] : g[u])
        if (mat[i][v] == mat[i][u] + w)
          clo[v] |= clo[u];
    for (int j = 1; j <= n; ++j) {
      if (in[i][j] || mat[i][j] == -1) continue;
      clo[j].reset(i);
      clo[j].reset(j);
      if ((in[i] & out[j] & clo[j]).any()) {
        in[i].set(j);
        out[j].set(i);
      } else
        ++ans;
    }
  }
  cout << ans << '\n';

#ifdef LBT
  LOG("Time: %dms\n", int ((clock()
          -nol_cl) / (double)CLOCKS_PER_SEC * 1000));
#endif
  return 0;
}

猜你喜欢

转载自blog.csdn.net/EI_Captain/article/details/103709107
pa
今日推荐