Solution Set【2024.1.10】
CF1919F1 Wine Factory (Easy Version) / CF1919F2 Wine Factory (Hard Version)
考虑使用网络流刻画这个问题,将每个工厂建一个对应的节点,连出以下三种边:
- 从源点向工厂连一条容量为 \(a_i\) 的边
- 从工厂向汇点连一条容量为 \(b_i\) 的边
- 从工厂 \(i\) 向工厂 \(i + 1\) 连一条容量为 \(c_i\) 的边
这样这个网络的最大流就是答案,但是直接使用最大流算法会超时,所以我们考虑观察该网络的性质以快速求出其最大流。
首先根据最大流最小割定理,我们可以知道最大流等于最小割,因此我们只需要求出最小割即可。
结论:对于每个 \(i\),边 \(S \rightarrow i\) 和边 \(i \rightarrow T\) 一定恰好有一条在最小割中。
下面证明该结论成立。
首先我们证明不可能存在两条边都不在最小割中的情况,由于流量均为非负整数,若这两条边中存在一条流量为 \(0\),那么将这条边加入最小割集对答案没有影响,因此我们只考虑两条边流量均不为 \(0\) 的情况。发现在这种情况下流量可以通过 \(S \rightarrow i \rightarrow T\),与割的定义矛盾,因此不存在两条边都不在最小割中的情况。
接下来考虑不存在两条边都在最小割中的情况,假设存在这样的最小割,那么我们按如下两种情况分类讨论:
-
若 \(S\) 可以不通过边 \(S \rightarrow\) 到达 \(i\),例如 \(S \rightarrow j \rightarrow j + 1 \rightarrow \cdots \rightarrow i\),那么无论边 \(S \rightarrow i\) 是否被割,对答案均没有影响,因此我们可以将边 \(S \rightarrow i\) 移出最小割集,得到一个不劣的方案。
-
若 \(S\) 必须通过边 \(S \rightarrow\) 到达 \(i\),那么只要我们确保边 \(S \rightarrow i\) 在最小割集中,那么就一定没有流量到达点 \(i\),因此可以将边 \(i \rightarrow T\) 移出最小割集,得到一个不劣的方案。
综上所述,我们可以得到结论:对于每个 \(i\),边 \(S \rightarrow i\) 和边 \(i \rightarrow T\) 一定恰好有一条在最小割中。
现在我们考虑如何维护全局的最小割。
不妨称 \(S \rightarrow i\) 的边为 \(\tt{A}\) 类边,\(i \rightarrow T\) 的边为 \(\tt{B}\) 类边,\(i \rightarrow i + 1\) 的边为 \(\tt{C}\) 类边。
可以发现上文中的结论可以改写为:对于每个 \(i\),\(\tt{A}\) 类边和 \(\tt{B}\) 类边一定恰好有一条在最小割中。
发现由于其对 \(\tt{C}\) 类边没有限制,因此其实际上为满足最小割的一个必要不充分条件,我们考虑如何表达 \(\tt{C}\) 类边的限制。
考虑对于一条形如 \(i \rightarrow i + 1\) 的 \(\tt{C}\) 类边,若 \(i\) 的 \(\tt{B}\) 类边在最小割中,同时 \(i + 1\) 的 \(\tt{A}\) 类边也在最小割中,那么这条边也一定在最小割中,因为若其不在最小割中,流量可以通过 \(S \rightarrow i \rightarrow i + 1 \rightarrow T\),与割的定义矛盾。可以发现对于其他的情况均对 \(\tt{C}\) 类边没有限制。
考虑使用线段树维护最小割,具体的,每个节点维护其对应区间 \(\left[l, r\right]\) 满足在 \(l\) 处选择 \(\tt{A}\) 类边或 \(\tt{B}\) 类边,在 \(r\) 处选择 \(\tt{A}\) 类边或 \(\tt{B}\) 类边共四种情况的最小代价,合并两区间时,枚举 \(l\) 和 \(r\) 的选择情况,根据上文的结论,可以得到合并后的区间的最小代价。这四种情况的最小值便是这个区间的最小割。
时间复杂度 \(\mathcal{O}\left(n \log n\right)\)。
Code
#pragma GCC optimize("Ofast")
#pragma GCC optimize("unroll-loops")
#include <bits/stdc++.h>
typedef long long valueType;
typedef std::vector<valueType> ValueVector;
typedef std::vector<ValueVector> ValueMatrix;
typedef std::array<std::array<valueType, 2>, 2> Matrix;
typedef std::vector<Matrix> MatrixVector;
constexpr valueType MAX = std::numeric_limits<valueType>::max() >> 2, MIN = std::numeric_limits<valueType>::min() >> 2;
class Tree {
private:
valueType N;
ValueVector C;
MatrixVector min;
void merge(valueType id, valueType mid) {
for (valueType l = 0; l < 2; ++l) {
for (valueType r = 0; r < 2; ++r) {
min[id][l][r] = std::min({min[id << 1][l][0] + min[id << 1 | 1][0][r],
min[id << 1][l][1] + min[id << 1 | 1][1][r],
min[id << 1][l][0] + min[id << 1 | 1][1][r],
min[id << 1][l][1] + min[id << 1 | 1][0][r] + C[mid]});
}
}
}
public:
Tree() = default;
Tree(valueType n) : N(n), C(n + 1), min((n << 2) + 10) {}
Tree(valueType n, ValueVector const &A, ValueVector const &B, ValueVector const &C) : N(n), C(C), min((n << 2) + 10) {
build(1, 1, n, A, B);
}
private:
void build(valueType id, valueType l, valueType r, ValueVector const &A, ValueVector const &B) {
if (l == r) {
min[id][0][0] = A[l];
min[id][1][1] = B[l];
min[id][0][1] = MAX;
min[id][1][0] = MAX;
return;
}
valueType const mid = (l + r) >> 1;
build(id << 1, l, mid, A, B);
build(id << 1 | 1, mid + 1, r, A, B);
merge(id, mid);
}
public:
valueType Ans() const {
return std::min({min[1][0][0], min[1][0][1], min[1][1][0], min[1][1][1]});
}
void Modify(valueType x, valueType a, valueType b, valueType c) {
C[x] = c;
update(1, 1, N, x, a, b);
}
private:
void update(valueType id, valueType nodeL, valueType nodeR, valueType pos, valueType A, valueType B) {
if (nodeL == nodeR) {
min[id][0][0] = A;
min[id][1][1] = B;
min[id][0][1] = MAX;
min[id][1][0] = MAX;
return;
}
valueType const mid = (nodeL + nodeR) >> 1;
if (pos <= mid)
update(id << 1, nodeL, mid, pos, A, B);
else
update(id << 1 | 1, mid + 1, nodeR, pos, A, B);
merge(id, mid);
}
};
int main() {
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
std::cout.tie(nullptr);
valueType N, M;
std::cin >> N >> M;
ValueVector A(N + 1), B(N + 1), C(N + 1);
for (valueType i = 1; i <= N; ++i)
std::cin >> A[i];
for (valueType i = 1; i <= N; ++i)
std::cin >> B[i];
for (valueType i = 1; i < N; ++i)
std::cin >> C[i];
Tree tree(N, A, B, C);
for (valueType m = 0; m < M; ++m) {
valueType x, a, b, c;
std::cin >> x >> a >> b >> c;
tree.Modify(x, a, b, c);
std::cout << tree.Ans() << '\n';
}
std::cout << std::flush;
return 0;
}
CF1919G Tree LGM
首先考虑另一个问题:
给定一棵树,如何得出矩阵 \(s\)。
考虑对于每一个节点为根进行求解,设 \(f_u\) 表示 \(u\) 节点是否满足先手必胜,可以发现,\(f_u = 1\) 当且仅当 \(\exists v \in \operatorname{son}_u \rightarrow f_v = 0\),即从 \(u\) 出发可以到达一个必败状态,反之我们有 \(f_u = 0\)。
考虑利用这一点来求解,首先可以发现,若 \(s_{u, u} = 0\),那么也就是说以 \(u\) 为根的情况下 \(u\) 的各个儿子节点都是必胜状态。若 \(s_{u, u} = 1\),那么这等同于说以 \(u\) 为根的情况下 \(u\) 的各个儿子节点中存在一个必败状态。
考虑根不同实际上限制了什么,可以发现根和当前节点的不同实际上是在当前节点可以转移到的状态当中移除了位于其到根节点的路径上的状态。因此若 \(s_{u, u} = 0\),那么无论移除哪个状态其均满足从 \(u\) 出发不可能到达必败状态,因此对于 \(1 \le j \le n\),有 \(s_{j, u} = 0\)。若 \(s_{u, u} = 1\),那么可以根据从 \(u\) 出发可以到达的必败状态的个数来确定 \(s_{j, u}\) 的值。
- 若从 \(u\) 出发可以到达的必败状态个数不小于 \(2\),那么无论移除哪个状态其均满足从 \(u\) 出发可以到达必败状态,因此对于 \(1 \le j \le n\),有 \(s_{j, u} = 0\)。
- 若从 \(u\) 出发可以到达的必败状态个数为 \(1\),那么我们可以将其移除,此时 \(u\) 的各个儿子节点均为必胜状态,因此不能保证对于 \(1 \le j \le n\),有 \(s_{j, u} = 1\)。不妨将点划分为两个集合 \(S, T\),满足 \(j \in S \rightarrow s_{j, u} = 1\) 且 \(j \in T \rightarrow s_{j, u} = 0\)。
发现第二种情况下的点集 \(T\) 实际上是以 \(u\) 为根下的一棵子树。若我们可以找到这棵子树中与 \(u\) 相连的点 \(v\),那么我们就可以确定原树的一条边 \(\left(u, v\right)\),进而可以将问题转化为 \(S, T\) 两个连通块的子问题,实现分治递归求解。
考虑如何找到满足要求的 \(v\),我们需要进一步挖掘性质。
首先我们有 \(s_{v, u} = 0\),那么我们可以推出 \(s_{v, v} = 1\),因为在以 \(v\) 为根的情况下一定可以到达必败状态 \(u\)。同时我们有 \(s_{u, v} = 0\),根据 \(v\) 的定义(从 \(u\) 出发可以到达的唯一必败节点)可以证明。我们可以将其扩展为对于 \(j \in T \leftrightarrow s_{j, v} = 1\)。对于其充分性不难发现只要 \(j \in S\),那么从 \(v\) 出发均可以到达必败状态 \(u\),因此 \(s_{j, v} = 1\)。对于其必要性,我们考虑证明 \(j \in S \rightarrow s_{j, v} = 0\),不难发现由于 \(u\) 处于 \(j\) 和 \(v\) 之间的路径上,因此从 \(v\) 出发不可能到达 \(u\),而剩余其可以到达的状态于以 \(u\) 为根时相同,因此 \(s_{j, v} = 0\),由于有 \(j \notin T \leftrightarrow j \in S\),所以我们可以得到 \(j \notin T \rightarrow s_{j, v} \neq 1\),结合 \(j \in T \rightarrow s_{j_v} = 1\) 可证。
同时可以发现对于 \(j \in T\) 但 \(j \neq v\),其一定不满足上述条件,这是因为即使其满足 \(s_{u, j} = 0\) 和 \(s_{j, j} = 1\),那么一定有 \(s_{v, j} = s_{u, j} = 0\),于上述条件矛盾。因此上述条件是充分必要的。
在找到 \(v\) 之后,我们还需要对于所有 \(i \in S, j \in T\),验证 \(s_{i, j}\) 和 \(s_{j, i}\) 的合法性才能继续分治递归求解。
具体的,对于 \(i \in S, j \in T\) 且 \(j \neq v\),我们有:
对于 \(i \in T, j \in S\) 且 \(j \neq u\),我们有:
上述两个条件可以通过设置根对可达节点的影响来证明。具体的,以第一条为例,可以发现 \(v\) 一定处于 \(i\) 和 \(j\) 之间的路径上,因此从 \(i\) 出发因为在其于 \(v\) 的路径上而被限制不能到达的状态,即使根节点改变,也不会影响其不可到达的性质,因此 \(i\) 的可达状态集合不改变,进而推知 \(s_{i, j}\) 的值不会改变。
进而我们现在可以得到一些边的集合和一些与这些边不交的点的集合,其中点集中的点一定是满足 \(\forall j \neq i \rightarrow s_{j, i} = s_{i, i}\) 的点集。我们将 \(s_{i, i} = 0\) 的点称为 \(0\) 类点,将 \(s_{i, i} = 1\) 的点称为 \(1\) 类点。根据定义可以发现 \(0\) 类点只能和 \(1\) 类点相连,而每个 \(1\) 类点至少和两个不同的 \(0\) 类点相连。可以发现按此规则下使用 \(0\) 类点个数最少的构造方案形如:
其中需要 \(0\) 类点的个数为 \(1\) 类点的个数加 \(1\),若 \(0\) 类点的个数不满足这个条件,那么一定无解。对于多余的 \(0\) 类点,将其随意连接到任意一个 \(1\) 类点即可。对于已经连边的点,可以发现其只能连接到 \(1\) 类点上,任选一个即可。
通过对每个 \(i\),使用 xor hash 维护出满足 \(s_{j, i} = 1\) 的 \(j\) 的集合,可以使得时间复杂度降为 \(\mathcal{O}(n^2)\),具体的,在找到边 \(\left(u, v\right)\) 并把图分为两个点集 \(S, T\) 的过程中,这两个点集之间的每个数对只会被遍历常数次,因此时间复杂度为 \(\mathcal{O}(n^2)\)。
Code
#include <bits/stdc++.h>
typedef int valueType;
typedef std::vector<valueType> ValueVector;
typedef std::vector<ValueVector> ValueMatrix;
typedef std::pair<valueType, valueType> ValuePair;
typedef std::vector<ValuePair> PairVector;
typedef std::vector<PairVector> PairMatrix;
typedef unsigned long long hashType;
typedef std::vector<hashType> HashVector;
typedef std::mt19937_64 Engine;
typedef std::vector<char> CharVector;
typedef std::vector<CharVector> CharMatrix;
typedef std::vector<bool> bitset;
valueType N;
PairVector G;
CharMatrix S;
hashType AllSet = 0;
HashVector WinableRootSet, HashWeight;
bitset done;
void failed() {
std::cout << "NO" << std::endl;
std::exit(0);
}
void solve(ValueVector V) {
valueType X = -1, Y = -1;
ValueVector InTree, OutTree;
for (auto const &u : V) {
if (S[u][u] == '0' || done[u] || WinableRootSet[u] == AllSet)
continue;
InTree.clear();
OutTree.clear();
for (auto const &v : V) {
if (S[v][u] == '1')
OutTree.push_back(v);
else
InTree.push_back(v);
}
if (!InTree.empty() && !OutTree.empty()) {
X = u;
break;
}
}
if (X == -1) {
ValueVector special, zero, one;
for (auto const &u : V) {
if (done[u])
special.push_back(u);
else if (S[u][u] == '0')
zero.push_back(u);
else
one.push_back(u);
}
for (valueType i = 1; i < special.size(); ++i)
G.emplace_back(special[i - 1], special[i]);
if (one.empty()) {
if (zero.empty())
return;
if (zero.size() >= 2 || !special.empty())
failed();
return;
}
if (one.size() >= zero.size())
failed();
for (valueType i = 0; i < one.size(); ++i) {
G.emplace_back(zero[i], one[i]);
G.emplace_back(one[i], zero[i + 1]);
}
for (valueType i = one.size() + 1; i < zero.size(); ++i)
G.emplace_back(one.front(), zero[i]);
if (!special.empty())
G.emplace_back(one.front(), special.front());
return;
}
for (auto const v : InTree) {
if (S[v][v] == '0' || done[v] || (WinableRootSet[v] ^ WinableRootSet[X]) != AllSet)
continue;
bool ok = true;
for (auto const &u : OutTree) {
if (S[u][v] != '0') {
ok = false;
break;
}
}
if (ok) {
Y = v;
break;
}
}
if (Y == -1)
failed();
for (auto const &u : OutTree) {
for (auto const &v : InTree) {
if (v != Y && S[u][v] != S[Y][v])
failed();
}
}
for (auto const &u : InTree)
for (auto const &v : OutTree)
if (v != X && S[u][v] != S[X][v])
failed();
G.emplace_back(X, Y);
done[X] = done[Y] = true;
solve(InTree);
solve(OutTree);
}
int main() {
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
std::cout.tie(nullptr);
std::cin >> N;
G.reserve(N - 1);
S.resize(N + 1, CharVector(N + 1));
done.resize(N + 1, false);
for (valueType i = 1; i <= N; ++i)
for (valueType j = 1; j <= N; ++j)
std::cin >> S[i][j];
HashWeight.resize(N + 1);
Engine engine(std::chrono::steady_clock::now().time_since_epoch().count() ^ std::random_device()() ^ (hashType) std::make_unique<char>().get() ^ 0xbeefc0ffee);
for (auto &x : HashWeight)
x = engine();
AllSet = std::accumulate(HashWeight.begin() + 1, HashWeight.begin() + N + 1, static_cast<hashType>(0), std::bit_xor<>());
WinableRootSet.resize(N + 1, 0);
for (valueType i = 1; i <= N; ++i)
for (valueType j = 1; j <= N; ++j)
if (S[i][j] == '1')
WinableRootSet[j] ^= HashWeight[i];
for (valueType i = 1; i <= N; ++i) {
if (S[i][i] == '1')
continue;
for (valueType j = 1; j <= N; ++j)
if (S[j][i] == '1')
failed();
}
ValueVector V(N);
std::iota(V.begin(), V.end(), 1);
solve(V);
std::cout << "YES" << std::endl;
for (auto const &[u, v] : G)
std::cout << u << ' ' << v << std::endl;
return 0;
}