230722 做题记录 // 网络流 24 题 (6/24)
知耻而后勇,物极必反。
下文称「拆点」操作为「保留节目」。
Day 1:1st - 6th
A. 星际转移问题
http://222.180.160.110:1024/contest/3952/problem/1
如果就按照题目给的路线图,我们显然无法考虑到飞船到达的时刻。同时 \(n\) 和 \(m\) 又很小,我们就知道了,「人不能两次踏进同一条河流」,\(1\) 时刻的站 \(p\) 和 \(2\) 时刻的站 \(p\) 也不能是同一个站 \(p\)。
考虑用 \((p, t)\) 表示 \(t\) 时刻的站 \(p\),然后对于每条路线跑个暴力连边,容量全部为 \(H_i\)。
怎么控制时间最小呢?二分一下就可以了…… 然后最大流判定是不是满流的即可。由于空间站容量为无穷大,甚至没有保留节目 保留节目不再是保留节目了呜呜呜
以及注意到对于同一站点,前面的时刻可以留下来等后面的时刻,我们将同一站的前一时刻和后一时刻全部连边,容量为 \(k\)。以及保留节目对源点拆点以控制流量为 \(k\)。
经实验答案最大为 29,所以把二分上界设为 30 即可 理论上来说答案可能很大,比如你谷最后一组数据的答案就是 \(900\) 多,所以我掐指一算用天才般的算术技巧开了 \(10^4\)。真的,数数位天才就是我。
woc,这题居然没人做,果然我还是太强了。
为什么都跑去做 T4 了,这个不是按难度顺序排列的吗?
哦哦,好像不是,那(Na)没(Mei)事(Shi)了(Le)。
#define int long long
namespace XSC062 {
using namespace fastIO;
const int lim = 1e4;
const int inf = 1e18;
const int maxm = 4e5 + 5;
const int maxn = 5e4 + 15;
struct _ {
int v, w, n;
_() {}
_(int v1, int w1, int n1) {
v = v1, w = w1, n = n1;
}
};
struct __ {
int c;
std::vector<int> p;
};
_ u[maxm];
__ w[maxn];
int h[maxn];
int l, mid, r;
int gs, gt, tot = 1;
int n, m, k, s, mt, x, res, y;
int vis[maxn], now[maxn], dep[maxn];
inline int fun(int p, int t) {
return (p - 1) * mt + t;
}
inline int min(int x, int y) {
return x < y ? x : y;
}
bool BFS(int n) {
std::fill(vis + 1, vis + n + 1, 0);
std::fill(dep + 1, dep + n + 1, 0);
std::queue<int> q;
dep[gs] = 1, vis[gs] = 1;
q.push(gs), now[gs] = h[gs];
while (!q.empty()) {
int f = q.front();
q.pop();
for (int i = h[f]; i; i = u[i].n) {
int v = u[i].v, w = u[i].w;
if (vis[v] == 1 || w == 0)
continue;
vis[v] = 1, now[v] = h[v];
dep[v] = dep[f] + 1, q.push(v);
if (v == gt)
return 1;
}
}
return 0;
}
int findP(int x, int flow = inf) {
if (x == gt)
return flow;
int rest = flow, i;
for (i = now[x]; rest && i; i = u[i].n) {
int v = u[i].v, w = u[i].w;
now[x] = i;
if (dep[v] != dep[x] + 1 || w == 0)
continue;
int t = findP(v, min(rest, w));
if (t == 0)
dep[v] = 0;
rest -= t;
u[i].w -= t, u[i ^ 1].w += t;
}
return flow - rest;
}
inline int Dinic(int n) {
int res = 0;
while (BFS(n)) {
int t = findP(gs);
while (t) {
res += t;
t = findP(gs);
}
}
return res;
}
inline void add(int x, int y, int w) {
u[++tot] = _(y, w, h[x]);
h[x] = tot;
return;
}
inline void addf(int x, int y, int w) {
add(x, y, w), add(y, x, 0);
return;
}
inline void Init(void) {
tot = 1;
memset(h, 0, sizeof (h));
return;
}
inline bool check(int x) {
Init();
mt = x, s = fun(n, mt) + 1;
gs = s + 1, gt = s + 2;
addf(gs, s, k);
for (int i = 1; i <= mt; ++i) {
addf(s, fun(n - 1, i), k);
addf(fun(n, i), gt, k);
}
for (int i = 1; i <= n; ++i) {
for (int j = 1; j < mt; ++j)
addf(fun(i, j), fun(i, j + 1), k);
}
for (int i = 1; i <= m; ++i) {
int p = 0, x = 0, la = 0;
while (++p <= mt) {
if (la != 0) {
addf(fun(la, p - 1),
fun(w[i].p[x], p), w[i].c);
}
la = w[i].p[x];
if (++x >= w[i].p.size())
x = 0;
}
}
return (Dinic(gt) == k);
}
int main() {
read(n), read(m), read(k);
for (int i = 1; i <= m; ++i) {
read(w[i].c), read(y);
while (y--) {
read(x);
if (x == 0)
x = n + 1;
else if (x == -1)
x = n + 2;
w[i].p.push_back(x);
}
}
n += 2;
l = 1, r = lim;
while (l <= r) {
mid = (l + r) >> 1;
if (check(mid)) {
res = mid;
r = mid - 1;
}
else l = mid + 1;
}
print(res ? res - 1 : 0, '\n');
return 0;
}
} // namespace XSC062
#undef int
B. 最长递增子序列
http://222.180.160.110:1024/contest/3952/problem/2
就算知道不是按难度顺序排列我也要顺序开题。欸嘿,就是玩。
第一问很水,跑个 DP 就行。
第二问有点意思,取出就代表只能选一次,没想到这次保留节目这么前,总之把每个数拆成入点和出点,容量为 \(1\),这样就可以只选一次了。
那怎么保证每次找到的流一定是 LIS 呢?其实这和我们 Dinic 的深度分层数组有异曲同工之妙,我们把 \(f_i = f_j + 1(i>j,A_i\ge A_j)\) 的 \((j, i)\) 连边,容量为 \(1\) 即可。
然后源点只和满足 \(f_x = 1\) 的 \(x\) 连边,相应地,汇点之和满足 \(f_x = \text{LIS}\) 的 \(x\) 连边。
第三问很好想啊,我们把 \(1\) 到源点和 \(n\) 到汇点的容量设成无穷大就好。
然后踩了半天的坑,这道题的保留节目部分不知道为什么必须要连双向边。
#define int long long
namespace XSC062 {
using namespace fastIO;
const int inf = 1e18;
const int maxm = 4e5 + 5;
const int maxn = 5e5 + 15;
struct _ {
int v, w, n;
_() {}
_(int v1, int w1, int n1) {
v = v1, w = w1, n = n1;
}
};
_ u[maxm];
int n, res;
int gs, gt, tot = 1;
int a[maxn], h[maxn], f[maxn];
int vis[maxn], now[maxn], dep[maxn];
inline int min(int x, int y) {
return x < y ? x : y;
}
inline int max(int x, int y) {
return x > y ? x : y;
}
bool BFS(int n) {
std::fill(vis + 1, vis + n + 1, 0);
std::fill(dep + 1, dep + n + 1, 0);
std::queue<int> q;
dep[gs] = 1, vis[gs] = 1;
q.push(gs), now[gs] = h[gs];
while (!q.empty()) {
int f = q.front();
q.pop();
for (int i = h[f]; i; i = u[i].n) {
int v = u[i].v, w = u[i].w;
if (vis[v] == 1 || w == 0)
continue;
vis[v] = 1, now[v] = h[v];
dep[v] = dep[f] + 1, q.push(v);
if (v == gt)
return 1;
}
}
return 0;
}
int findP(int x, int flow = inf) {
if (x == gt)
return flow;
int rest = flow, i;
for (i = now[x]; rest && i; i = u[i].n) {
int v = u[i].v, w = u[i].w;
now[x] = i;
if (dep[v] != dep[x] + 1 || w == 0)
continue;
int t = findP(v, min(rest, w));
if (t == 0)
dep[v] = 0;
rest -= t;
u[i].w -= t, u[i ^ 1].w += t;
}
return flow - rest;
}
inline int Dinic(int n) {
int res = 0;
while (BFS(n)) {
int t = findP(gs);
while (t) {
res += t;
t = findP(gs);
}
}
return res;
}
inline void add(int x, int y, int w) {
u[++tot] = _(y, w, h[x]);
h[x] = tot;
return;
}
inline void addf(int x, int y, int w) {
add(x, y, w), add(y, x, 0);
return;
}
int main() {
read(n);
gs = 2 * n + 1, gt = gs + 1;
for (int i = 1; i <= n; ++i) {
read(a[i]);
f[i] = 1;
addf(i, i + n, 1);
addf(i + n, i, 1);
for (int j = 1; j < i; ++j) {
if (a[j] <= a[i])
f[i] = max(f[i], f[j] + 1);
}
res = max(res, f[i]);
for (int j = 1; j < i; ++j) {
if (a[j] <= a[i] &&
f[i] == f[j] + 1)
addf(j, i + n, 1);
}
}
for (int i = 1; i <= n; ++i) {
if (f[i] == 1)
addf(gs, i, 1);
if (f[i] == res)
addf(i + n, gt, 1);
}
print(res, '\n');
print(Dinic(gt), '\n');
tot = 1;
memset(h, 0, sizeof (h));
for (int i = 1; i <= n; ++i) {
addf(i, i + n, 1);
addf(i + n, i, 1);
for (int j = 1; j < i; ++j) {
if (a[j] <= a[i] &&
f[i] == f[j] + 1)
addf(j, i + n, 1);
}
}
for (int i = 1; i <= n; ++i) {
if (f[i] == 1) {
if (i == 1)
addf(gs, i, inf);
else addf(gs, i, 1);
}
if (f[i] == res) {
if (i == n)
addf(i + n, gt, inf);
else addf(i + n, gt, 1);
}
}
print(Dinic(gt), '\n');
return 0;
}
} // namespace XSC062
#undef int
C. 餐巾计划问题
http://222.180.160.110:1024/contest/3952/problem/3
这个有点简单啊。就差把费用流三个大字拍你脸上了。
用过和没用过的餐巾是不能混合处理的,故考虑保留节目,把每一天拆出来一个点表示当天所有用过的餐巾量(注意不止是当天用过的,还可以是前几天传下来的)。
首先不难想到大源点和每天的没用过连边,容量为无穷大,费用为购买费用,表示购买餐巾;用过的和下一天用过的连边,容量为无穷大,费用为 \(0\),表示用过的餐巾的继承;用过的和快洗 / 慢洗所需时间后的没用过的连边,容量为无穷大,费用为快洗 / 慢洗费用,表示把用过的洗成没用过的(奇奇怪怪)。
那么问题来了,怎么表示使用餐巾呢?这里有一个很妙的处理方式,把没用过的朝大汇点连边,容量为当天使用量,费用为 \(0\),表示把这么多没用过的餐巾销毁;再把大源点朝用过的连边,容量也为当天使用量,费用为 \(0\),表示凭空变出来这么多条用过的餐巾。
然后跑个费用流就可以了。
#define int long long
namespace XSC062 {
using namespace fastIO;
const int inf = 1e18;
const int maxn = 4e3 + 5;
const int maxm = 6e5 + 5;
struct _ {
int v, c, w, n;
_() {}
_(int v1, int c1, int w1, int n1) {
v = v1, c = c1, w = w1, n = n1;
}
};
_ u[maxm];
bool inq[maxn];
int gs, gt, tot = 1;
int h[maxn], dis[maxn];
int fl[maxn], pre[maxn];
int n, m, t1, c1, t2, c2, res, x;
inline int min(int x, int y) {
return x < y ? x : y;
}
inline bool SPFA(int s, int n) {
std::queue<int> q;
std::fill(dis + 1, dis + n + 1, inf);
q.push(s), dis[s] = 0, inq[s] = 1;
pre[s] = inf, pre[gt] = 0, fl[s] = inf;
while (!q.empty()) {
int f = q.front();
q.pop(), inq[f] = 0;
for (int i = h[f]; i; i = u[i].n) {
if (u[i].c == 0)
continue;
int v = u[i].v, w = u[i].w;
if (dis[v] > dis[f] + w) {
pre[v] = i ^ 1;
dis[v] = dis[f] + w;
fl[v] = min(fl[f], u[i].c);
if (!inq[v])
inq[v] = 1, q.push(v);
}
}
}
return pre[gt];
}
inline void SSP(int s, int n) {
int p, mn, d;
while (SPFA(s, n)) {
mn = fl[gt], d = 0;
for (p = gt; p != s; p = u[pre[p]].v) {
u[pre[p]].c += mn;
u[pre[p] ^ 1].c -= mn;
d += u[pre[p] ^ 1].w;
}
res += mn * d;
}
return;
}
inline void add(int x, int y, int c, int w) {
u[++tot] = _(y, c, w, h[x]);
h[x] = tot;
return;
}
inline void addf(int x, int y, int c, int w) {
add(x, y, c, w), add(y, x, 0, -w);
return;
}
int main() {
read(n);
gs = 2 * n + 1, gt = gs + 1;
for (int i = 1; i <= n; ++i) {
read(x);
addf(i, gt, x, 0);
addf(gs, i + n, x, 0);
if (i != n)
addf(i + n, i + 1 + n, inf, 0);
}
read(m), read(t1);
read(c1), read(t2), read(c2);
for (int i = 1; i <= n; ++i) {
addf(gs, i, inf, m);
if (i + t1 <= n)
addf(i + n, i + t1, inf, c1);
if (i + t2 <= n)
addf(i + n, i + t2, inf, c2);
}
SSP(gs, gt);
print(res, '\n');
return 0;
}
} // namespace XSC062
#undef int
D. 运输问题
http://222.180.160.110:1024/contest/3952/problem/4
不是很理解啊,这题就一个普普通通的二分图建模,有什么难点吗,,,
哦,蓝的,那没事了。
#define int long long
namespace XSC062 {
using namespace fastIO;
const int inf = 1e18;
const int maxn = 4e3 + 5;
const int maxm = 6e5 + 5;
struct _ {
int v, c, w, n;
_() {}
_(int v1, int c1, int w1, int n1) {
v = v1, c = c1, w = w1, n = n1;
}
};
bool inq[maxn];
int gs, gt, tot = 1;
_ u[maxm], u1[maxm];
int fl[maxn], pre[maxn];
int h[maxn], dis[maxn], h1[maxn];
int n, m, t1, c1, t2, c2, res, x;
inline int min(int x, int y) {
return x < y ? x : y;
}
inline bool SPFA(int s, int n) {
std::queue<int> q;
std::fill(dis + 1, dis + n + 1, inf);
q.push(s), dis[s] = 0, inq[s] = 1;
pre[s] = inf, pre[gt] = 0, fl[s] = inf;
while (!q.empty()) {
int f = q.front();
q.pop(), inq[f] = 0;
for (int i = h[f]; i; i = u[i].n) {
if (u[i].c == 0)
continue;
int v = u[i].v, w = u[i].w;
if (dis[v] > dis[f] + w) {
pre[v] = i ^ 1;
dis[v] = dis[f] + w;
fl[v] = min(fl[f], u[i].c);
if (!inq[v])
inq[v] = 1, q.push(v);
}
}
}
return pre[gt];
}
inline bool SPFA1(int s, int n) {
std::queue<int> q;
std::fill(pre + 1, pre + n + 1, 0);
std::fill(dis + 1, dis + n + 1, -inf);
q.push(s), dis[s] = 0, inq[s] = 1;
pre[s] = inf, pre[gt] = 0, fl[s] = inf;
while (!q.empty()) {
int f = q.front();
q.pop(), inq[f] = 0;
for (int i = h[f]; i; i = u[i].n) {
if (u[i].c == 0)
continue;
int v = u[i].v, w = u[i].w;
if (dis[v] < dis[f] + w) {
pre[v] = i ^ 1;
dis[v] = dis[f] + w;
fl[v] = min(fl[f], u[i].c);
if (!inq[v])
inq[v] = 1, q.push(v);
}
}
}
return pre[gt];
}
inline void SSP(int s, int n) {
int p, mn, d;
while (SPFA(s, n)) {
mn = fl[gt], d = 0;
for (p = gt; p != s; p = u[pre[p]].v) {
u[pre[p]].c += mn;
u[pre[p] ^ 1].c -= mn;
d += u[pre[p] ^ 1].w;
}
res += mn * d;
}
return;
}
inline void SSP1(int s, int n) {
int p, mn, d;
while (SPFA1(s, n)) {
mn = fl[gt], d = 0;
for (p = gt; p != s; p = u[pre[p]].v) {
u[pre[p]].c += mn;
u[pre[p] ^ 1].c -= mn;
d += u[pre[p] ^ 1].w;
}
res += mn * d;
}
return;
}
inline void add(int x, int y, int c, int w) {
u[++tot] = _(y, c, w, h[x]);
h[x] = tot;
return;
}
inline void addf(int x, int y, int c, int w) {
add(x, y, c, w), add(y, x, 0, -w);
return;
}
int main() {
read(n), read(m);
gs = n + m + 1, gt = gs + 1;
for (int i = 1; i <= n; ++i) {
read(x);
addf(gs, i, x, 0);
}
for (int i = 1; i <= m; ++i) {
read(x);
addf(i + n, gt, x, 0);
}
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= m; ++j) {
read(x);
addf(i, j + n, inf, x);
}
}
memcpy(h1, h, sizeof (h1));
memcpy(u1, u, sizeof (u1));
SSP(gs, gt);
print(res, '\n'), res = 0;
memcpy(h, h1, sizeof (h));
memcpy(u, u1, sizeof (u));
SSP1(gs, gt);
print(res, '\n');
return 0;
}
} // namespace XSC062
#undef int
You could go the distance
You could run the mile
You could walk straight through hell with a smile
—— Hall of Fame
虽然说我现在的处境不算地狱吧,但是我周边有恶魔这是真的,我笑而不语也是真的。
好吧,我又笑又语。Welcome to hell, b**ch.
E. 最小路径覆盖
http://222.180.160.110:1024/contest/3952/problem/5
当我们把所有点视作长度为 \(0\) 的路径时,答案为 \(n\)。怎么让这个答案减小呢?我们需要 合并路径。
假设有路径 \(u \to x\) 和 \(x \to v\),那么将它们合并为 \(u\to v\) 显然可以得到更优的答案。
那么这个时候就有同学要问了,我选择合并路径的方式会不会对答案产生影响呢?这个不急,我们讲完了再证明。
首先开一个新图,把所有点整一个保留节目,\(S\to x\) 建一条容量为 \(1\) 的边;\(x' \to T\) 建一条容量为 \(1\) 的边;对于边 \(u\to v\),在 \(u\to v'\) 建一条容量为 \(1\) 的边。
这个时候我们就有了一个类二分图的模型。想想看,我们在里面跑出来的最大流是什么?
在这种容量均为 \(1\) 的类二分图模型中,网络流中找到的路径其实就是二分图中的增广路,因为反向的容量为 \(0\) 的边就相当于已匹配边,会限制搜索进一步搜下去。
在二分图中,每找到一条增广路,最大匹配的大小便扩大 \(1\);在这里的网络流中也一样,每找到一条路径,最大流的大小便扩大 \(1\)。
那么,这里的「路径」究竟有什么含义?
一条从 \(S\) 到 \(T\) 的边,若其流量为 \(1\),我们将它视作原图中被选中、加入路径集合的边,你会发现,找最大流(不断延长路径)的过程就相当于在合并路径,而且最后这个新图满足:点和边都不会被重复选中,且因为我们找的是最大流,所有点都会被选中。
好好好,正确性就很显而易见了,来自于二分图增广路找最大匹配的正确性(被打)。
那么求出最大流 \(f\),因为每合并一次路径,路径的条数就会减少 \(1\),所以最后的答案就是 \(N - f\)。方案呢?
因为网络流特性,你会发现路径的起点一定是 \(S\to x\)(废话),所以找出所有 \(S\to x\) 流量为 \(1\) 的 \(x\),它们就是每条路径的起点。
因为路径没有交叉且肯定联通,所以你沿着这个起点一直找流量为 \(1\) 的边就能找到头。
namespace XSC062 {
using namespace fastIO;
const int inf = 1e18;
const int maxn = 4e3 + 5;
const int maxm = 6e5 + 5;
struct _ {
int v, w, n;
_() {}
_(int v1, int w1, int n1) {
v = v1, w = w1, n = n1;
}
};
_ u[maxm];
bool vis1[maxn];
int n, m, x, y, res;
int gs, gt, tot = 1;
int a[maxn], h[maxn], f[maxn];
int vis[maxn], now[maxn], dep[maxn];
inline int min(int x, int y) {
return x < y ? x : y;
}
inline int max(int x, int y) {
return x > y ? x : y;
}
bool BFS(int n) {
std::fill(vis + 1, vis + n + 1, 0);
std::fill(dep + 1, dep + n + 1, 0);
std::queue<int> q;
dep[gs] = 1, vis[gs] = 1;
q.push(gs), now[gs] = h[gs];
while (!q.empty()) {
int f = q.front();
q.pop();
for (int i = h[f]; i; i = u[i].n) {
int v = u[i].v, w = u[i].w;
if (vis[v] == 1 || w == 0)
continue;
vis[v] = 1, now[v] = h[v];
dep[v] = dep[f] + 1, q.push(v);
if (v == gt)
return 1;
}
}
return 0;
}
int findP(int x, int flow = inf) {
if (x == gt)
return flow;
int rest = flow, i;
for (i = now[x]; rest && i; i = u[i].n) {
int v = u[i].v, w = u[i].w;
now[x] = i;
if (dep[v] != dep[x] + 1 || w == 0)
continue;
int t = findP(v, min(rest, w));
if (t == 0)
dep[v] = 0;
rest -= t;
u[i].w -= t, u[i ^ 1].w += t;
}
return flow - rest;
}
inline int Dinic(int n) {
int res = 0;
while (BFS(n)) {
int t = findP(gs);
while (t) {
res += t;
t = findP(gs);
}
}
return res;
}
inline void add(int x, int y, int w) {
u[++tot] = _(y, w, h[x]);
h[x] = tot;
return;
}
inline void addf(int x, int y, int w) {
add(x, y, w), add(y, x, 0);
return;
}
void output(int x) {
if (x == gs)
return;
print(x, ' ');
vis1[x] = 1;
for (int i = h[x]; i; i = u[i].n) {
int v = u[i].v;
if (v <= n || v > 2 * n || vis1[v - n])
continue;
if (u[i].w == 0) {
output(u[i].v - n);
return;
}
}
return;
}
int main() {
read(n), read(m);
gs = 2 * n + 1, gt = gs + 1;
for (int i = 1; i <= n; ++i) {
addf(gs, i, 1);
addf(i + n, gt, 1);
}
while (m--) {
read(x), read(y);
addf(x, y + n, 1);
}
res = n - Dinic(gt);
for (int i = h[gt]; i; i = u[i].n) {
if (u[i ^ 1].w == 1) {
output(u[i].v - n);
putchar('\n');
}
}
print(res, '\n');
return 0;
}
} // namespace XSC062
#undef int
F. 数字梯形
http://222.180.160.110:1024/contest/3952/problem/6
在 GM 的强制要求下只能跳了,呜呜呜
Day 2:7th - 14th
A. 太空飞行计划
—— · EOF · ——
真的什么也不剩啦 😖