CSU-ACM2025 春季训练赛-第三场 题解

写在前面

CSU-ACM2025 春季训练赛-第三场
Begin: 2025-03-26 13:00 UTC+8,End: 2025-03-26 15:30 UTC+8

DIV1:https://vjudge.net/contest/704355
DIV2:https://vjudge.net/contest/704356

Password:ЗвездапоимениСолнце

涉及的套路:

  • Div2B:交互题;
  • Div2D,Div1A:比较大数、乘积、幂的套路,取 log
  • Div1D:全局异或运算对 01Trie 的影响是交换左右儿子;
  • Div1E:有关 \(\gcd\) 的计数,考虑枚举 \(d=\gcd\),然后容斥+整除分块;
  • Div2F:一类有状态转换的最优化题目,考虑图论建模,使用节点表示当前状态,转化为最短路问题;
  • Div2G,Div1F:一类有关状态合法性的状压 DP,预处理合法/不合法状态后,自底向上递推;

Div2A 洛谷 P3383

【模板】 线性筛素数

Div2B 洛谷 P1733

猜数(IO交互版)

学习一下交互题怎么做。

If in the first act you have hung a pistol on the wall, then in the following one it should be fired. Otherwise don't put it there.[1]

Div2C 洛谷 P8306

【模板】 字典树

If in the first act you have hung a pistol on the wall, then in the following three it can also be fired. Otherwise don't put it there.[2]

Div1A,Div2H 洛谷 P5854

【模板】笛卡尔树

Div2D,Div1B CodeForces 1995C

1800 维护技巧 2.trick。

以下提供一种套路做法。

发现可以直接从前往后操作,每次将当前位置的数调整为最小的、不小于前一个数的数即可。然而大力平方之后会很大,没法直接比较大小那么没法直接做。

发现 \(a_i\) 平方 \(c_i\) 次后为 \(a^{2^{c_i}}\),于是仅需考虑如何找到最小的 \(c'_i\),使得:

\[\large a_i^{2^{c'_i}} \ge a_{i -1}^{2^{c_{i-1}}} \]

这个幂次的形式太典了,取个 \(\log\)

\[2^{c'_i}\log a_i \ge 2^{c_{i-1}} \log a_{i -1} \]

发现有 \(2\) 的幂还是很大,不太好直接比较,那么再取个 \(\log\)

\[c'_i\log 2 + \log\log a_i \ge c_{i-1}\log 2 +\log \log a_{i -1} \]

这下就好做了。每次二分答案,或者直接大力推式子算出来最小的 \(c'\) 即可,若求不出来则无解。

以下实现使用了二分。

//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
const int kN = 2e5 + 10;
const double eps = 1e-9;
//=============================================================
int n, a[kN]; 
LL ans, f[kN];
//=============================================================
bool notsmaller(LL a_, LL f1_, LL b_, LL f2_) {
  double lga = log10(1.0 * a_), lgb = log10(1.0 * b_);
  double lglga = log10(lga), lglgb = log10(lgb);
  double lg2 = log10(2.0);

  return (f1_ * lg2 + lglga + eps >= f2_ * lg2  + lglgb);
}
//=============================================================
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;
    for (int i = 1; i <= n; ++ i) std::cin >> a[i];
    for (int i = 1; i <= n; ++ i) f[i] = 0;

    ans = 0;
    for (int i = 2; i <= n; ++ i) {
      if (notsmaller(a[i], 0, a[i - 1], f[i - 1])) continue;
      for (LL l = 1, r = 1e10; l <= r; ) {
        LL mid = (l + r) >> 1ll;
        if (notsmaller(a[i], mid, a[i - 1], f[i - 1])) {
          f[i] = mid;
          r = mid - 1;
        } else {
          l = mid + 1;
        }
      }
      if (!f[i]) {
        ans = -1;
        break;
      }
      ans += f[i];
    }
    std::cout << ans << "\n";
  }
  return 0;
}

Div2E,Div1C CodeForces 1934C

1700 交互 5.人类智慧

限定四次询问,一个显然的想法是仅考虑询问矩形的四个顶点,此时每次询问得到的可能有地雷的区域是一条对角线,形态比较优美,而且如果有交点的话交点是地雷的概率很大。

若图中只有一个地雷那非常简单,仅需询问 \((1, 1)\)\((1, m)\),得到的两条可能存在地雷的对角线的交点即为答案,使用两次询问即可。

然而若有两个地雷上述做法就不行了,可能使得第二次询问被另一个地雷影响,使得两条对角线上没有地雷、两条对角线无交点、或两条对角线交点在矩形之外。不过好在还有两次询问,考虑对上述结果分类讨论一下:

  • 若对角线有交点,则询问交点;
  • 若没有交点,或者询问后交点上没有地雷,则比较第一次和第二次询问答案哪个小:
    • 若第一次小则再询问 \((n, 1)\) 并与第一次询问的对角线求交点;
    • 否则询问 \((n, m)\) 并与第二次询问的对角线求交点。
    • 可以一定可以求出交点,且交点处一定有地雷。

求交点设坐标解方程即可。

//交互
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
//=============================================================
int n, m;
//=============================================================
int query(int x_, int y_) {
  std::cout << "? " << x_ << " " << y_ << "\n";
  fflush(stdout);
  int x; std::cin >> x;
  return x;
}
std::pair <int, int> Judge1(int d1_, int d2_) {
  if ((d1_ - d2_ + m + 1) % 2 == 1) return std::make_pair(-1, -1);
  int y = (d1_ - d2_ + m + 1) / 2, x = d1_ - y + 2;
  if (y <= 0 || x <= 0) return std::make_pair(-1, -1);

  int ret3 = query(x, y);
  if (ret3 == 0) return std::make_pair(x, y);
  return std::make_pair(0, 0);
}
//=============================================================
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;
    int ret1 = query(1, 1), ret2 = query(1, m);
    if (ret1 == 0) {
      std::cout << "! " << 1 << " " << 1 << "\n";
      fflush(stdout);
      continue;
    } 
    if (ret2 == 0) {
      std::cout << "! " << 1 << " " << m << "\n";
      fflush(stdout);
      continue;
    }

    std::pair <int,int> r = Judge1(ret1, ret2);
    if (r.first > 0) {
      std::cout << "! " << r.first << " " << r.second << "\n";
      fflush(stdout);
      continue;
    }

    if (ret1 < ret2) {
      int ret4 = query(n, 1);
      if (ret4 == 0) {
        std::cout << "! " << n << " " << 1 << "\n";
      } else {
        int x = (ret1 - ret4 + n + 1) / 2, y = ret1 - x + 2;
        std::cout << "! " << x << " " << y << "\n";
      }
      fflush(stdout);
    } else {
      int ret4 = query(n, m);
      if (ret4 == 0) {
        std::cout << "! " << n << " " << m << "\n";
      } else {
        int x = (ret2 - ret4 + n + 1) / 2, y = x - 1 + m - ret2;
        std::cout << "! " << x << " " << y << "\n";  
      }
      fflush(stdout);
    }
  }
  return 0;
}

Div1D CodeForces 1895D

1900 二进制,构造,Trie 2.套路,trick 4:须魔改某一算法的题

显然有前缀和 \(\operatorname{pre}_i = b_1\oplus b_{i + 1}\),因为保证有解,说明 \(\operatorname{pre}_i\) 是两两不相等的,且均不等于 0,但是可能大于 \(n-1\);于是仅需考虑如何找到 \(0\le b_1\le n - 1\),使得 \(b_i\) 均不大于 \(n-1\) 即可,从而使 \(b_1\sim b_n\) 恰好为 \(0\sim n - 1\)

\(\operatorname{pre}_0 = 0\),问题也可以看做构造 \(0\le b_1\le n - 1\),使得 \(\forall 0\le i\le n - 1, \operatorname{pre}_i \oplus b_1 = b_{i + 1}\) 恰好为 \(0\sim n - 1\)

这显然是个典,考虑放到 Trie 上考虑,将 \(\operatorname{pre}_i\) 先插入到 Trie 里,则令所有数某一位异或 1 相当于交换这一位对应层的上一层所有节点的左右儿子,问题即通过交换儿子操作使得 Trie 变为一棵满二叉树。因为保证有解,于是仅需自顶向下构造,每次考虑当前节点左右儿子的 \(\operatorname{size}\) 与最终状态的 \(\operatorname{size}\) 的关系,若不相等则交换儿子,然后仅考虑最右侧的非空的子树进行递归构造即可。

然后就能构造出 \(b_1\) 了。总时间复杂度 \(O(n\log n)\) 级别。

实际实现时可以不需要显式地建出字典树,只要能实现上述等价操作即可。

//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
const int kN = 2e5 + 10;
//=============================================================
int n, a[kN], pre[kN], b[kN];
//=============================================================
namespace Trie {
  const int kNode = kN << 5;
  int nodenum = 1, sz[kNode], tr[kNode][2];
  void insert(int val_) {
    int now = 1;
    for (int i = 20; ~i; -- i) {
      ++ sz[now];

      int x = ((val_ >> i) & 1);
      if (!tr[now][x]) tr[now][x] = ++ nodenum;
      now = tr[now][x];
    }
    ++ sz[now];
  }
  int check() {
    int ret = 0, cnt = 0, now = 1;
    for (int i = 20; ~i; -- i) {
      int sz0 = sz[tr[now][0]];
      int yes0 = std::min(n - cnt, (1 << i)), yes1 = std::max(0, n - cnt - (1 << i));
      if (sz0 != yes0) {
        ret ^= (1 << i);
        std::swap(tr[now][0], tr[now][1]);
      }
      if (yes1) now = tr[now][1], cnt += (1 << i);
      else now = tr[now][0];
    }
    return ret;
  }
}
//=============================================================
int main() {
  // freopen("1.txt", "r", stdin);
  std::ios::sync_with_stdio(0), std::cin.tie(0);
  std::cin >> n;
  
  Trie::insert(0);
  for (int i = 1; i < n; ++ i) {
    std::cin >> a[i], pre[i] = pre[i - 1] ^ a[i];
    Trie::insert(pre[i]);
  }
  int b0 = Trie::check();

  std::cout << b0 << " ";
  for (int i = 1; i < n; ++ i) {
    std::cout << (b0 ^ pre[i]) << " ";
  }
  return 0;
}
/*
13
9 2 1 9 5 1 15 9 5 6 14 9
*/

Div1E CodeForces 1780E

2400 数学,讨论,整除分块 2.trick

这里有一张 \(n=10^{18}\) 的完全图,点有点权,边有边权。第 \(i\) 个节点的点权值为 \(i\),边 \((u,v)\) 的边权值为 \(\gcd(u,v)\)
\(t\) 组数据,每组数据给定整数 \(l,r\),求由节点 \(l\sim r\) 构成的完全子图中边权的种类数。
\(1\le t\le 100\)\(1\le l\le r\le 10^{18}\)\(l\le 10^9\)
2S,256MB。

前置知识:整除分块

问题实质是求 \([l, r]\) 中的数两两 \(\gcd\) 的种类数。考虑枚举 \(d = \gcd(i, j) (l\le i<j\le r)\)

对于 \(d\ge l\) 时,显然当 \(2\times d \le r\) 时,\(d\) 对答案有贡献,则有贡献的 \(d\) 满足的条件为:

\[d\in \left[l, \left\lfloor\frac{r}{2}\right\rfloor\right] \]

对于 \(d< l\),考虑 \([l,r]\)\(d\) 的倍数的位置。显然其中最小的 \(d\) 的倍数为 \(\left\lceil \frac{l}{d} \right\rceil \times d\),最大的倍数为 \(\left\lfloor \frac{r}{d} \right\rfloor \times d\)。当满足 \(\left\lceil \frac{l}{d} \right\rceil < \left\lfloor \frac{r}{d} \right\rfloor\)\(d\) 对答案有贡献。由整除分块可知 \(\left\lceil \frac{l}{d} \right\rceil\)\(\left\lfloor \frac{r}{d} \right\rfloor\) 分别只有 \(\sqrt{l}\)\(\sqrt{r}\) 种取值,但 \(r\) 过大,我们考虑通过整除分块枚举所有 \(\left\lceil \frac{l}{d} \right\rceil\) 相等的区间 \([i,j]\)

对于 \(d\in [i,j]\),当 \(d\) 递增时也有 \(\left\lfloor \frac{r}{d} \right\rfloor\) 单调递减成立,则可以考虑在区间 \([i,j]\) 上二分得到最大的满足 \(\left\lceil \frac{l}{d} \right\rceil < \left\lfloor \frac{r}{d} \right\rfloor\)\(d\),区间 \([i,j]\) 对答案有贡献的数的取值范围即 \([i, d]\)\(O(\sqrt{l})\) 地枚举所有区间后再 \(O(\log l)\) 地累计贡献即可,总复杂度 \(O(\sqrt{l}\log l)\) 级别,可以通过本题。

或者更进一步地,对于 \(d\in [i,j]\),满足上述条件实质上等价于 \(\left(\left\lceil \frac{l}{d} \right\rceil + 1\right)\times d \le r\)。枚举区间后 \(\left\lceil \frac{l}{d} \right\rceil\) 的值固定,则最大的 \(d\) 为:

\[d = \min\left(\frac{r}{\left(\left\lceil \frac{l}{d} \right\rceil + 1\right)}, j\right) \]

注意这样计算出的 \(d\) 可能小于 \(i\),需要特判一下。此时总复杂度变为 \(O(\sqrt{l})\) 级别。

//By:Luckyblock
/*
*/
#include <cmath>
#include <cstdio>
#include <cctype>
#include <algorithm>
#define LL long long
//=============================================================
//=============================================================
inline LL read() {
	LL f = 1, w = 0; char ch = getchar();
	for (; !isdigit(ch); ch = getchar()) if (ch == '-') f = - 1;
	for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + ch - '0';
	return f * w;
}
//=============================================================
int main() {
  // freopen("1.txt", "r", stdin);
  int T = read();
  while (T --) {
    LL l = read(), r = read(), ans = 0, p = 0;
    if (r / 2 >= l) ans += r / 2 - l + 1;
    for (LL j = l - 1, i; j; j = i - 1) {
      i = ceil(1.0 * l / ceil(1.0 * l / j));
      LL k = ceil(1.0 * l / j);
      ans += std::max(0ll, std::min(r / (k + 1), j) - i + 1);
    }
    printf("%lld\n", ans);
  }
  return 0;
}

Div2F CodeForces 1937E

2400 最短路,图论建模转化 2.trick 4.须魔改某一算法的题

馆长为了道馆真是想象力丰富啊!

居然是牛逼建图最短路,而且这个图建得有一种自动机的味儿,学到了。

首先对于某属性强化操作的终点是离散的,一定是某个宝可梦该属性值。

经过强化后宝可梦的属性是递增的,且召唤完之后还需要把它打败,则所有宝可梦至多只会召唤一次,若出现召唤多次的情况说明有一段操作是冗余甚至有害的(使当前被召唤多次的宝可梦的属性上升从而不好打败了)。

考虑最优的操作序列的形态:(加强 \(a_1\) 直至能打败 1,召唤 \(a_1\) 打败 1) \(\longrightarrow\) (加强 \(a_2\) 直至能打败 \(a_1\),召唤 \(a_2\) 打败 \(a_1\)\(\longrightarrow \cdots \longrightarrow\) (加强 \(a_n\) 直至能打败 \(a_k\),召唤 \(n\) 打败 \(a_k\))。

考虑把这个操作序列倒过来考虑,如果把当前在道馆的宝可梦看做结点,问题好像变为找到一条 \(n\rightarrow 1\) 的最短的有向路径,有向边代表前一个宝可梦打败后一个,于是考虑建图跑最短路:

  • 对于每个宝可梦 \(i\) 的属性 \(j\),建立节点 \(X_{i, j}\),代表召唤该宝可梦后准备使用其属性 \(j\);建立 \(Y_{i, j}\) 表示可以打败该宝可梦。
  • 连有向边 \((i, X_{i, j}, c_i)\) 表示召唤,\((X_{i, j}, Y_{i, j}, 0)\) 表示可以打败属性不大于自身的,\((Y_{i, j}, i, 0)\) 表示打败该宝可梦后使该宝可梦可用来召唤。
  • 对于每种属性 \(j\),将所有宝可梦按照该属性值升序排序,设排序后宝可梦顺序为 \(b_{1}\sim b_n\),则连边 \((X_{b_{i-1}, j}, X_{b_{i}, j}, a_{b_{i}, j} - a_{b_{i - 1}, j})\) 表示强化操作至第一个大于的宝可梦;连边 \((Y_{b_{i}, j}, Y_{b_{i - 1}, j}, 0)\) 表示可以打败更弱的宝可梦。

然后跑出 \(n\rightarrow 1\) 的最短路即为答案。

节点与边数均为 \(O(nm)\) 级别,总时间复杂度 \(O(nm\log {nm})\) 级别。

//知识点:建图,最短路
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
#define pr std::pair
#define mp std::make_pair
const int kN = 2e6 + 10;
const LL kInf = 1e18 + 2077;
//=============================================================
int n, m, nodenum;
int edgenum, head[kN], v[kN << 1], w[kN << 1], ne[kN << 1];
int c[kN];
std::vector <std::vector <pr <int, int> > > a;
LL dis[kN];
bool vis[kN];
//=============================================================
inline int read() {
  int f = 1, w = 0; char ch = getchar();
  for (; !isdigit(ch); ch = getchar()) if (ch == '-') f = -1;
  for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0'); 
  return f * w;
}
void Add(int u_, int v_, int w_) {
  v[++ edgenum] = v_;
  w[edgenum] = w_;
  ne[edgenum] = head[u_];
  head[u_] = edgenum;
}
void Dijkstra() {
  std::priority_queue <pr <LL, int> > q;
  for (int i = 1; i <= nodenum; ++ i) dis[i] = kInf, vis[i] = 0;
  q.push(mp(0, n));
  dis[n] = 0;
  
  while (!q.empty()) {
    int u_ = q.top().second; q.pop();
    if (vis[u_]) continue;
    vis[u_] = true;
    for (int i = head[u_]; i; i = ne[i]) {
      int v_ = v[i], w_ = w[i];
      if (dis[u_] + w_ < dis[v_]) {
        dis[v_] = dis[u_] + w_;
        q.push(mp(-dis[v_], v_));
      }
    }
  }
}
void Init() {
  for (int i = 1; i <= nodenum; ++ i) {
    head[i] = 0;
  }
  nodenum = edgenum = 0;
  
  nodenum = n = read(), m = read();
  for (int i = 1; i <= n; ++ i) c[i] = read();
  a.clear(), a.push_back(std::vector <pr <int, int> >());
  for (int i = 1; i <= m; ++ i) {
    a.push_back(std::vector <pr <int, int> >(1, mp(0, 0)));
  }
  for (int i = 1; i <= n; ++ i) {
    for (int j = 1; j <= m; ++ j) {
      a[j].push_back(mp(read(), i));
    }
  }

  for (int i = 1; i <= m; ++ i) {
    std::sort(a[i].begin(), a[i].end());
    for (int j = 1; j <= n; ++ j) {
      int x = ++ nodenum;
      int y = ++ nodenum;
      Add(x, y, 0);
      Add(a[i][j].second, x, c[a[i][j].second]);
      Add(y, a[i][j].second, 0);
      if (j != 1) Add(x - 2, x, a[i][j].first - a[i][j - 1].first);
      if (j != 1) Add(y, y - 2, 0);
    }
  }
}
//=============================================================
int main() {
  // freopen("1.txt", "r", stdin);
  int T = read();
  while (T --) {
    Init();  
    // for (int i = 1; i <= nodenum; ++ i) {
    //   for (int j = head[i]; j; j = ne[j]) {
    //     printf("%d %d %d\n", i, v[j], w[j]);
    //   }
    // }
    Dijkstra();
    printf("%lld\n", dis[1]);
  }
  return 0;
}
/*
1
3 1
2
6
1
*/

Div2G,Div1F CodeForces 1995D

2300 维护技巧,状压 DP 2.trick 4.须魔改某一算法的题

江队说是套路妈的,场上光想着怎么 check 一个状态是否合法了还能这么搞太牛逼了。

这个数据范围疯狂暗示需要状压,于是考虑状压 DP 求有哪些选择结尾的状态是合法的。

发现题目给定限制,等价于对于所有长度为 \(k\) 的子区间,要求这些子区间中至少有一个字符为结尾字符;且最后一个字符一定为结尾字符。则对于每一个子区间中所有字符,在任一合法的选择结尾的方案中不能同时不出现

于是一个显然的想法是考虑在结尾字符中,不存在哪些字符会使得方案非法。记 \(f_{s}\) 表示不存在的结尾字符的状态 \(s\) 时是否合法,初始化:

  • 对于每一长为 \(k\) 的子区间,求其中出现的字符状态集合 \(s'\),则 \(f_{s'} = \text{false}\)
  • 仅包含最后一个字符 \(s_{n}\) 的状态 \(s''\),有 \(f_{s''} = \text{false}\)
  • 除此之外所有其他集合 \(s\) 均有 \(f_{s} = \text{true}\)

然后按照不存在字符数量枚举集合 \(1\sim 2^{c} - 1\),考虑当前集合是否包含一个非法的子集即可,即有转移:

\[\forall 1<s\le 2^{c}-1,\ f_{s} = \operatorname{AND}\limits_{2^i\in s} f_{s - 2^i} \]

最后对所有 \(f_{s} = \text{true}\) 的状态统计其中 0 的个数取最小值即为答案。

总时间复杂度 \(O(cn + c\times 2^c)\) 级别。

//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
const int kN = 1e6 + 10;
const int kInf = 1e9 + 2077;
const int kC = 20;
//=============================================================
int n, c, k, all, ans;
int cnt[kC];
bool can_delete[kN];
std::string s;
//=============================================================
void init() {
  all = (1 << c) - 1, ans = c;
  for (int i = 0; i <= all; ++ i) can_delete[i] = 1;
  for (int i = 0; i < c; ++ i) cnt[i] = 0;
  for (int i = 0; i < k; ++ i) ++ cnt[s[i] - 'A'];
  for (int l = 0, r = k - 1; r < n; ++ l, ++ r) {
    int j = 0;
    for (int i = 0; i < c; ++ i) if (cnt[i]) j |= (1 << i);
    can_delete[j] = 0;
    if (r + 1 < n) -- cnt[s[l] - 'A'], ++ cnt[s[r + 1] - 'A'];
  }

  can_delete[1 << (s[n - 1] - 'A')] = 0;
}
void solve() {
  for (int i = 1; i < all; ++ i) {
    int cnt0 = 0;
    for (int j = 0; j < c; ++ j) {
      if (i >> j & 1) can_delete[i] &= can_delete[i ^ (1 << j)];
      else ++ cnt0;
    }
    if (can_delete[i]) ans = std::min(ans, cnt0);
  }
}
//=============================================================
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 >> c >> k;
    std::cin >> s;
    init();
    solve();
    std::cout << ans << "\n";
  }
  return 0;
}

Div1F CodeForces 1993E

2700 结论,异或,状压 DP 2.trick 4.须魔改某一算法的题 5.人类智慧

先考虑一维的情况。

若只有一维,每次操作的结果和 [AGC016D] XOR Replace 是一样的。对 \(a_i\) 进行一次操作相当于令 \(a_i:=\oplus_{1\le i\le n} a_i\),再对 \(j\) 进行一次操作相当于令 \(a_j:= a_i\)

则题意等价于有一个长度为 \(n + 1\) 的数列 \(a\)\(a_{n + 1} = \oplus_{1\le i\le n} a_i\),可以任意交换 \(a_i\)\(a_{n + 1}\),对于 \(a_1\sim a_n\) 求相邻两项差的绝对值之和的最小值。

发现数据范围很小,且贡献仅与相邻元素有关,考虑状压 DP 构造数列,记 \(f_{s, i}\) 表示当前填入的数列元素集合为 \(s\),填入的最后一个数是 \(a_i\) 时美丽值的最小值。初始化 \(f_{s, i} = \infin, f_{\{i\}, i} = 0\),则有显然的转移:

\[\forall 1\le i, j\le n + 1, i\not= j, i\in s,\ \ f_{s, i}\rightarrow f_{s \cup \{j\}, j} + |a_{i} - a_{j}| \]

记全集为 \(S\),答案即为:

\[\min_{1\le i,j\le n+1, i\not= j} f_{S - \{i\}, j} \]

总时间复杂度 \(O(n^2 2^n)\) 级别。

扩展到两维,发现若先进行一次行操作再进行一次列操作,等价于将交点位置修改为整个矩阵的异或和。于是考虑扩展上述做法,题意等价于有一个大小为 \((n + 1)\times (m + 1)\) 的矩阵,第 \(n+1\) 行为各列的异或和,第 \(m+1\) 列为各行的异或和(\((n + 1, m + 1)\) 即整个矩阵异或和),每次操作可以交换两行/两列,求左上角 \(n\times m\) 矩阵的美丽值的最小值。

考虑预处理任意两行/两列相邻时的贡献。发现两维的贡献是独立的,不同维的交换并不影响另一维的贡献。发现若枚举了哪一行被放在了 \(n+1\) 行上,则对列的贡献的计算就可以直接套用一维的做法了。于是考虑转移时处理 \(\operatorname{sum}(i, j)\) 表示将第 \(i\) 行第 \(j\) 列时的最小美丽值,取最小值即可。

在一维做法的基础上仅需再多枚举一维即可,总时间复杂度 \(O(n^2m 2^n + nm^2 2^m) \approx O(n^3 2^n)\) 级别。

//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
const int kN = 16 + 2;
const int kS = (1 << 16) + 10;
const LL kInf = 1e18;
//=============================================================
int n, m, a[kN][kN];
LL ans, all, row[kN][kN], col[kN][kN], f[kS][kN], g[kS][kN];
LL sum[kN][kN];
//=============================================================
void init() {
  a[n + 1][m + 1] = 0;
  for (int i = 1; i <= n; ++ i) {
    a[i][m + 1] = 0;
    for (int j = 1; j <= m; ++ j) {
      a[i][m + 1] ^= a[i][j];
      a[n + 1][m + 1] ^= a[i][j];
    }
  }
  for (int j = 1; j <= m; ++ j) {
    a[n + 1][j] = 0;
    for (int i = 1; i <= n; ++ i) {
      a[n + 1][j] ^= a[i][j];
    }
  }
  for (int i = 1; i <= n + 1; ++ i) {
    for (int j = 1; j <= n + 1; ++ j) {
      row[i][j] = 0;
      for (int k = 1; k <= m + 1; ++ k) 
        row[i][j] += abs(a[i][k] - a[j][k]);
    }
  }
  for (int i = 1; i <= m + 1; ++ i) {
    for (int j = 1; j <= m + 1; ++ j) {
      col[i][j] = 0;
      for (int k = 1; k <= n + 1; ++ k)
        col[i][j] += abs(a[k][i] - a[k][j]);
    }
  }
}
void DP() {
  for (int i = 1; i <= n + 1; ++ i) {
    for (int j = 1; j <= m + 1; ++ j) {
      sum[i][j] = 0;
    }
  }

  all = (1 << (n + 1));
  for (int lst = 1; lst <= m + 1; ++ lst) {
    for (int s = 1; s < all; ++ s) {
      for (int i = 1; i <= n + 1; ++ i) {
        f[s][i] = kInf;
      }
    }
    for (int i = 1; i <= n + 1; ++ i) f[1 << (i - 1)][i] = 0;
    for (int s = 1; s < all; ++ s) {
      for (int i = 1; i <= n + 1; ++ i) {
        if ((s >> (i - 1) & 1) == 0) continue;
        for (int j = 1; j <= n + 1; ++ j) {
          if (i == j || (s >> (j - 1) & 1)) continue;
          f[s | (1 << (j - 1))][j] = std::min(f[s | (1 << (j - 1))][j], 
                                              f[s][i] + row[i][j] - abs(a[i][lst] - a[j][lst]));
        }
      }
    }
    for (int i = 1; i <= n + 1; ++ i) {
      LL minf = kInf;
      for (int j = 1; j <= n + 1; ++ j) {
        if (i == j) continue;
        minf = std::min(minf, f[(all - 1) ^ (1 << (i - 1))][j]);
      }
      sum[i][lst] += minf;
    }
  }
  
  all = (1 << (m + 1));
  for (int lst = 1; lst <= n + 1; ++ lst) {
    for (int s = 1; s < all; ++ s) {
      for (int i = 1; i <= m + 1; ++ i) {
        f[s][i] = kInf;
      }
    }
    for (int i = 1; i <= m + 1; ++ i) f[1 << (i - 1)][i] = 0;
    for (int s = 1; s < all; ++ s) {
      for (int i = 1; i <= m + 1; ++ i) {
        if ((s >> (i - 1) & 1) == 0) continue;
        for (int j = 1; j <= m + 1; ++ j) {
          if (i == j || (s >> (j - 1) & 1)) continue;
          f[s | (1 << (j - 1))][j] = std::min(f[s | (1 << (j - 1))][j], 
                                              f[s][i] + col[i][j] - abs(a[lst][i] - a[lst][j]));
        }
      }
    }
    for (int i = 1; i <= m + 1; ++ i) {
      LL minf = kInf;
      for (int j = 1; j <= m + 1; ++ j) {
        if (i == j) continue;
        minf = std::min(minf, f[(all - 1) ^ (1 << (i - 1))][j]);
      }
      sum[lst][i] += minf;
    }
  }

  ans = kInf;
  for (int i = 1; i <= n + 1; ++ i) {
    for (int j = 1; j <= m + 1; ++ j) {
      ans = std::min(ans, sum[i][j]);
    }
  }
}
//=============================================================
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;
    for (int i = 1; i <= n; ++ i) {
      for (int j = 1; j <= m; ++ j) {
        std::cin >> a[i][j];
      }
    }
    init();
    DP();
    std::cout << ans << "\n";
  }
  return 0;
}
/*
1
1 2
1 3
*/

写在最后

没题了!


  1. Gurliand, Ilia (11 July 1904). "Reminiscences of A.P. Chekhov". Театр и искусство (Teatr i iskusstvo - Theater and art) (28): 521. ↩︎

  2. What's the fucking meaning? ↩︎

posted @ 2025-03-24 16:49  Luckyblock  阅读(74)  评论(0)    收藏  举报