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\),使得:
这个幂次的形式太典了,取个 \(\log\):
发现有 \(2\) 的幂还是很大,不太好直接比较,那么再取个 \(\log\):
这下就好做了。每次二分答案,或者直接大力推式子算出来最小的 \(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< 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\) 可能小于 \(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\),考虑当前集合是否包含一个非法的子集即可,即有转移:
最后对所有 \(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\),则有显然的转移:
记全集为 \(S\),答案即为:
总时间复杂度 \(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
*/
写在最后
没题了!