ARC176 补
A 01 Matrix Again
求一个 \(n\times n\) 的 \(01\) 矩阵,使得满足限制的情况下,每行每列都有 \(m\) 个 \(1\)。
限制:给定 \(m\) 个坐标,在这些坐标上的数必须是 \(1\)。
Solution
思考一下怎么在全为 \(0\) 的矩阵中放 \(1\) 使得每行每列有且只有一个 \(1\)。不难想到对角线。
但是只有一种方案,显然是不够也无法满足条件的。注意到可以用两条斜线分别将其所在的每行每列 \(+1\)。加上对角线总共有 \(n\) 中方案,显然可以满足所有可能的 \(m\)。再注意一下发现其实一个方案所包含的格子就是 \((j-i+n) \mod n\) 相同的格子集合。
加上限制的话,可以看强制 \(1\) 的格子在哪个 \(+1\) 方案里,转化成强制使用这个方案。当然,可以有多个限制格在同一个方案中,所以如果每行每列 \(1\) 个数不到 \(m\) 的话还要随便补一点。
\(\\\)
Code
// STOOOOOOOOOOOOOOOOOOOOOOOOO hzt CCCCCCCCCCCCCCCCCCCCCCCORZ
#include <algorithm>
#include <iostream>
#include <numeric>
#include <set>
using namespace std;
using LL = long long;
using PII = pair<int, int>;
int n, m;
set<int> s;
int main() {
cin.tie(0)->sync_with_stdio(0);
cin >> n >> m;
for (int i = 1, a, b; i <= m; i++) {
cin >> a >> b, s.insert((b - a + n) % n);
}
for (int i = 0; s.size() < m; i++) {
s.insert(i);
}
cout << n * m << '\n';
for (auto d : s) {
for (int i = 1; i <= n; i++) {
cout << i << ' ' << (i + d - 1) % n + 1 << '\n';
}
}
return 0;
}
B Simple Math 4
求 \(2^n\mod (2^m-2^k)\) 的个位。
Solution
分类讨论,但是大部分情况都是弱智情况,注意一下就好。这里只给出 \(n \geq m > k + 1\) 的情况。
约分掉 \(2^k\),令 \(a=n-k,b=m-k\),原式 \(=\frac{2^a}{2^b-1}\)。
因为要求这个余数,而且一个是 \((10000...00)_2\),一个是 \((1111...11)_2\),不如竖式列出来看一眼。
先算一个位,\(2^a - 2^{a-b} \times (2^b-1) = 2^{a-b}\)。每除一位指数 \(-b\),直到无法再除,指数比 \(b\) 小时便结束,所以余数是 \(2^{a\mod b}\)。因为前面约分了一个 \(2^k\),算余数时要乘回来。
\(\\\)
Code
// STOOOOOOOOOOOOOOOOOOOOOOOOO hzt CCCCCCCCCCCCCCCCCCCCCCCORZ
#include <algorithm>
#include <iostream>
#include <numeric>
#include <vector>
using namespace std;
using LL = long long;
using PII = pair<int, int>;
constexpr int kP2[] = {6, 2, 4, 8};
LL T, n, m, k;
int main() {
cin.tie(0)->sync_with_stdio(0);
for (cin >> T; T--;) {
cin >> n >> m >> k;
if (m == k + 1) {
cout << (n >= k ? 0 : kP2[n % 4]) << '\n';
} else if (n < m) {
cout << kP2[n % 4] << '\n';
} else {
cout << kP2[(n - k) % (m - k) % 4] * kP2[k % 4] % 10 << '\n';
}
}
return 0;
}
C Max Permutation
求满足条件的 \(n\) 排列 \(p\) 数量。
条件:对于所有给定三元组 \((a_i,b_i,c_i)\),有 \(\max(p_{a_i},p_{b_i}) = c_i\)。
Solution
我 IQ 不太够,所以只能分讨。
题面具象化一下,\(a_i\) 和 \(b_i\) 连边表示 \(p\) 中位置 \(a_i\) 和 \(b_i\) 中有一个等于 \(c_i\),另一个严格小于。
不如按 \(c_i\) 从大到小枚举点权处理贡献,算完了就删掉确定的点和边。这样子将数从大到小确定可以更好的考虑边限制。
手玩一下发现要将当前枚举到的点权分成三种情况讨论:
-
不存在边权为这个点权的边。直接把这个点权赋给一个度为 \(0\) 的点。不存在就无解。
-
有且仅有一条为这个点权的边。把这个点权赋给这条边上度为 \(1\) 的点,即仅连了这条边的点。不存在就无解。
-
至少有两条为这个点权的边。若这些边组成一个菊花,且菊花的中心没有其他边权的边,则把点权赋给菊花中心。
证明非常简单。
\(\\\)
Code
// STOOOOOOOOOOOOOOOOOOOOOOOOO hzt CCCCCCCCCCCCCCCCCCCCCCCORZ
#include <algorithm>
#include <iostream>
#include <numeric>
#include <vector>
using namespace std;
using LL = long long;
using PII = pair<int, int>;
constexpr int kN = 2e5 + 1, kP = 998244353;
int n, m, d[kN];
vector<PII> e[kN];
int main() {
cin.tie(0)->sync_with_stdio(0);
cin >> n >> m;
for (int i = 1, u, v, w; i <= m; i++) {
cin >> u >> v >> w;
e[w].emplace_back(u, v);
d[u]++, d[v]++;
}
int tot = count(d + 1, d + n + 1, 0), ans = 1;
for (int i = n; i >= 1; i--) {
if (e[i].empty()) {
if (!tot) {
cout << "0\n";
return 0;
}
ans = 1ll * ans * tot % kP;
tot--;
} else if (e[i].size() == 1) {
auto &[u, v] = e[i][0];
d[u]--, d[v]--;
if (d[u] > 0 && d[v] > 0) {
cout << "0\n";
return 0;
}
d[u] || d[v] || (ans = 2 * ans % kP, tot++);
} else {
int t = 0;
auto [u, v] = e[i][0];
auto [p, q] = e[i][1];
if (u == p || u == q) {
t = u;
} else if (v == p || v == q) {
t = v;
} else if (!t) {
cout << "0\n";
return 0;
}
for (auto [x, y] : e[i]) {
if (x != t && y != t) {
cout << "0\n";
return 0;
}
}
for (auto [x, y] : e[i]) {
x == t && (swap(x, y), 0);
(--d[x]) || (tot++);
}
d[t] = 0;
}
}
cout << ans << '\n';
return 0;
}
D - Swap Permutation
给定一个 \(n\) 排列 \(p\),进行 \(m\) 次操作,求所有可能的操作方案所得序列权值和。
操作:任意选择排列中两数交换。
权值:排列中相邻两数差的绝对值的和。
Solution
可以考虑两个数在进行操作后可能发生的改变。
现在有两个数 \(a, b\) 和 \(n-2\) 个其他的数 \(c\)。则它们可能组合出的状态有 \((a,b),(b,a),(a,c),(c,a),(c,b),(b,c),(c,c)\) 七种可能。
所以可以做一个 \(7*7\) 的转移矩阵,表示这其中状态之间的转化,暴力转移算贡献就好了。
也可以换一种方法考虑,把每个数拆成一个 01 序列,第 \(i\) 位表示这个数是否达到 \(i\)。那么两个数的 01 序列中不同的位数就是差了。
所以可以考虑 01 之间可能变成的状态,有 \((0,0),(1,0),(0,1),(1,0)\),但是 \((0,1),(1,0)\) 贡献相同可以合并考虑。
于是可以枚举一个阈值,每次加入阈值上的数,又可以矩乘计算贡献。
\(\\\)
Code
// STOOOOOOOOOOOOOOOOOOOOOOOOO hzt CCCCCCCCCCCCCCCCCCCCCCCORZ
#include <algorithm>
#include <array>
#include <atcoder/modint>
#include <iostream>
#include <numeric>
#include <vector>
using namespace std;
using namespace atcoder;
using LL = long long;
using mint = modint998244353;
using PII = pair<int, int>;
using mat = array<array<mint, 3>, 3>;
constexpr int kN = 2e5 + 1;
mat operator*(mat a, mat b) {
mat ret = {{{0, 0, 0}, {0, 0, 0}, {0, 0, 0}}};
for (int k = 0; k < 3; k++) {
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
ret[i][j] += a[i][k] * b[k][j];
}
}
}
return ret;
}
mat P(mat a, int b) {
mat ret = {{{1, 0, 0}, {0, 1, 0}, {0, 0, 1}}};
for (; b; b /= 2) {
if (b % 2 == 1) {
ret = ret * a;
}
a = a * a;
}
return ret;
}
int n, m, a[kN], p[kN];
int main() {
cin.tie(0)->sync_with_stdio(0);
cin >> n >> m;
for (int i = 1; i <= n; i++) {
cin >> a[i];
p[a[i]] = i;
}
mint ans = 0;
mat st{{{0, 0, n - 1}, {0, 0, 0}, {0, 0, 0}}};
for (int i = 1; i < n; ++i) {
if (p[i] > 1) {
bool t = a[p[i] - 1] > i;
st[0][t]++, st[0][t + 1]--;
}
if (p[i] < n) {
bool t = a[p[i] + 1] > i;
st[0][t]++, st[0][t + 1]--;
}
mat F{{{n * (n - 1ll) / 2 - 2 * (n - i), 2 * (n - i), 0},
{i - 1, n * (n - 1ll) / 2 - n + 2, n - i - 1},
{0, 2 * i, n * (n - 1ll) / 2 - 2 * i}}};
ans += (st * P(F, m))[0][1];
}
cout << ans.val() << '\n';
return 0;
}
E - Max Vector
给定两个长度为 \(n\) 的序列 \(x,y\),和一个 \(m\times n\) 矩阵 \(a\)。对于每个 \(i\in [1,m]\) 都进行一次操作,求最后 \(x,y\) 中所有数之和的最小值。
操作:选择 \(x\) 或者 \(y\),将其每一位替换为这一位与 \(a_i\) 相同位的最大值。
\(1\leq n\leq 10,1\leq m,V\leq 500\)。
Solution
直接贪心,对于每次操作的选择,我直接看放哪边当前更好。
是错误的。因为当 \(a_1={V,V,0,0},a_2={0,V,V,0},a_3={0,0,V,V}\) 时,全给 \(x\) 或者全给 \(y\) 为最优,但是直接贪心会出现各种各样选择问题。
所以贪心没前途,看到两个操作必选一个,直接无脑最小割。暂时先不考虑 \(y\),先探究一下这个 \(a_{i,j}\) 对 \(x_i\) 最终值的限制怎么考虑。
看到 \(n\) 和 \(V\) 都非常的小,于是直接对于每个 \(x_i\) 建立一条长度为 \(V\) 的链,边权由 \(V\) 到 \(1\) 递减,在 \(x_i\) 的链割掉边权为 \(l\) 的边代表着最终 \(x_i=l\)。源点向每条链底部连边,边权 \(\infty\),禁止割。至于 \(a_{i,j}\) 的限制,直接由源点向 \(x_j\) 链上第 \(a_{i, j}\) 个点连边权 \(\infty\) 的边禁止割即可。
那加入 \(y\) 又怎么办?
不如将 \(x\) 的所有东西反向,对于每个 \(y_i\) 建一条边权由 \(1\) 到 \(V\) 递增的链,顶部连接汇点。重新考虑 \(a_{i,j}\) 限制。此时可以对于每个 操作 建立 一个 虚拟点,对于每个 \(j\),\(x_{j,a_{i,j}}\rightarrow op_i \rightarrow y_{j,a_{i,j}}\),边权 \(\infty\)。这样如果虚拟点被割到源点一侧说明第 \(i\) 次操作给了 \(x\),否则割到汇点一侧则给了 \(y\)。
还要考虑上 \(x,y\) 初始值,直接源汇分别连对应值 \(\infty\) 边即可。
\(\\\)
Code
// STOOOOOOOOOOOOOOOOOOOOOOOOO hzt CCCCCCCCCCCCCCCCCCCCCCCORZ
#include <algorithm>
#include <atcoder/all>
#include <iostream>
#include <numeric>
#include <vector>
using namespace std;
using namespace atcoder;
using LL = long long;
using PII = pair<int, int>;
constexpr int kN = 10 + 1, kM = 500 + 1, kV = 501, kI = 1e9;
int n, m, X[kN], Y[kN], a[kM][kN];
int S = 0, T = 1, x[kN][kV + 1], y[kN][kV + 1], op[kM], cnt = 1;
int main() {
cin.tie(0)->sync_with_stdio(0);
cin >> n >> m;
for (int i = 1; i <= n; i++) {
cin >> X[i];
iota(x[i], x[i] + kV + 1, cnt);
cnt += kV;
}
for (int i = 1; i <= n; i++) {
cin >> Y[i];
iota(y[i], y[i] + kV + 1, cnt);
cnt += kV;
}
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
cin >> a[i][j];
}
op[i] = ++cnt;
}
mf_graph<int> g(cnt + 1);
auto A = [&](int x, int y, int w) { g.add_edge(x, y, w); };
for (int i = 1; i <= n; i++) {
A(S, x[i][kV], kI);
A(y[i][kV], T, kI);
for (int j = 1; j < kV; j++) {
A(x[i][j + 1], x[i][j], j);
A(y[i][j], y[i][j + 1], j);
}
A(x[i][X[i]], T, kI);
A(S, y[i][Y[i]], kI);
}
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
A(x[j][a[i][j]], op[i], kI);
A(op[i], y[j][a[i][j]], kI);
}
}
cout << g.flow(S, T) << '\n';
return 0;
}