大三第九周学习笔记

该博客记录了一周内算法题的解题思路。涉及树形dp、线段树、网络流、二分答案、状压dp、贪心等多种算法。如周二的city safety题有树形dp和最大权闭合子图两种解法;周三的最小路径覆盖问题用网络流求解;周四的Boss Rush题采用二分答案和状压dp。

周二

H. city safety(树形dp/最大权闭合子图)

比赛时想到了树形dp的做法,但是不知道怎么统计w,而且当前设置的范围会影响父亲。

其实是没想清楚,用dp[u][j]表示以u为根的子树每个点的贡献之和,注意这里不包括父亲那边,但转移状态时会限制父亲的范围,所以是不会错的。

#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

typedef long long ll;
const int N = 200 + 10;
int w[N], v[N], n;
vector<int> g[N];
ll dp[N][N];

void dfs(int u, int fa)
{
	_for(i, 1, n) dp[u][i] = v[i] - w[u];
	for(int v: g[u])
	{
		if(v == fa) continue;
		dfs(v, u);

		_for(i, 0, n)
		{
			ll cur = -1e18;
			for(int j = max(i - 1, 0); j <= min(i + 1, n); j++)
				cur = max(cur, dp[v][j]);
			dp[u][i] += cur;
		}
	}
}

int main()
{
	scanf("%d", &n);
	_for(i, 1, n) scanf("%d", &w[i]);
	_for(i, 1, n) scanf("%d", &v[i]);
	_for(i, 1, n - 1)
	{
		int u, v;
		scanf("%d%d", &u, &v);
		g[u].push_back(v);
		g[v].push_back(u);
	}

	dfs(1, 0);

	ll ans = 0;
	_for(i, 0, n) ans = max(ans, dp[1][i]);
	printf("%lld\n", ans);

    return 0;
}

然后还有最大权闭合子图的写法,但是要限制一些东西。

网络流的点数可以跑1e4~1e5左右的数据

首先这题一个点选了一个范围其他点九必须选,这个范围带来的权值是正的,必须选带来的范围是负的,容易想到最大权闭合子图,关键是怎么构图

对于每个点,开n个点,表示不同范围的收益,那么显然这n个点只能选一个,贡献只能算一次,这该怎么表示呢

把每个点的贡献变成增量收益,即第i个点的权值是v[i] - v[i - 1],那么这样设权值的话就要设置前i个点全选,后面全部不选。

那么我们可以从i+1连一条容量为正无穷的边到i,这样子的话,标号大的点就会优先流满,流满就是删边就是不选,所以选的是一个前缀

#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

typedef long long ll;
const int N = 5e4;
struct Edge { int from, to, flow; };
vector<Edge> edge;
vector<int> g[N];
int d[N], cur[N];
int n, m, s, t;

void add(int from, int to ,int flow)
{
	edge.push_back({from, to, flow});
	g[from].push_back(edge.size() - 1);
	edge.push_back({to, from, 0});
	g[to].push_back(edge.size() - 1);
}

bool bfs()
{
	memset(d, 0, sizeof d);
	queue<int> q;
	q.push(s);
	d[s] = 1;

	while(!q.empty())
	{
		int u = q.front(); q.pop();
		for(int x: g[u])
		{
			Edge e = edge[x];
			if(!d[e.to] && e.flow)
			{
				d[e.to] = d[u] + 1;
				q.push(e.to);
			}
		}
	}
	return d[t];
}

ll dfs(int u, ll in)
{
	if(u == t) return in;
	ll out = 0;
	for(int& i = cur[u]; i < g[u].size(); i++)
	{
		Edge& e = edge[g[u][i]];
		if(d[u] + 1 == d[e.to] && e.flow)
		{
			ll f = dfs(e.to, min((ll)e.flow, in));
			e.flow -= f;
			edge[g[u][i] ^ 1].flow += f;
			out += f; in -= f;
			if(in == 0) break;
		}
	}
	return out;
}

const int M = 210;
ll w[M], v[M], dis[M][M];

int id(int i, int j)
{
	return i * n + j + 1;
}

void build() 
{
	scanf("%d", &n);
	_for(i, 1, n) scanf("%lld", &w[i]);
	rep(i, 0, n) scanf("%lld", &v[i]);

	memset(dis, 0x3f, sizeof dis);
	_for(i, 1, n) dis[i][i] = 0;
	_for(i, 1, n - 1)
	{
		int u, v;
		scanf("%d%d", &u, &v);
		dis[u][v] = dis[v][u] = 1;
	}
	_for(k, 1, n)
		_for(i, 1, n)
			_for(j, 1, n)
				dis[i][j] = min(dis[i][j], dis[i][k] + dis[k][j]);
	
	s = n * n + n + 1;
	t = s + 1;

	_for(i, 1, n)
		_for(j, 0, n - 1)
		{
			if(!j) add(s, id(i, j), v[j]);
			else
			{
				add(s, id(i, j), v[j] - v[j - 1]);
				add(id(i, j), id(i, j - 1), 1e18);
			}
			_for(k, 1, n)
				if(dis[i][k] == j)
					add(id(i, j), k, 1e18);
		}
	_for(i, 1, n) add(i, t, w[i]);
}

int main()
{
	build();

	ll ans = 0;
	while(bfs())
	{
		memset(cur, 0, sizeof cur);
		ans += dfs(s, 1e18);
	}
	printf("%lld\n", n * v[n - 1] - ans);

    return 0;
}

Static Query on Tree(线段树)

换了一种写法

增加一个专门用来清空的lazy tag,这样不容易出错

#include <bits/stdc++.h>
#define l(k) (k << 1)
#define r(k) (k << 1 | 1)
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
 
const int N = 2e5 + 10;
int ta[N << 2], tab[N << 2];
int lazya[N << 2], lazyb[N << 2], lazy_clear[N << 2];
int d[N], fa[N], top[N], id[N], siz[N], son[N], cnt;
vector<int> g[N];
int n, q;
 
void dfs1(int u, int father)
{
	d[u] = d[father] + 1;
	fa[u] = father;
	siz[u] = 1;
	for(int v: g[u])
	{
		dfs1(v, u);
		siz[u] += siz[v];
		if(siz[son[u]] < siz[v]) son[u] = v;
	}
}
 
void dfs2(int u, int fa, int t)
{
	top[u] = t;
	id[u] = ++cnt;
	
	if(siz[u] == 1) return;
	dfs2(son[u], u, t);
 
	for(int v: g[u])
	{
		if(v == fa || v == son[u]) continue;
		dfs2(v, u, v);
	}
}

void updateA(int k, int l, int r)
{
	ta[k] = r - l + 1;
	lazya[k] = 1;
}
 
void updateB(int k, int l, int r)
{
	tab[k] = ta[k];
	lazyb[k] = 1;
}

void cla(int k, int l, int r)
{
	ta[k] = tab[k] = lazya[k] = lazyb[k] = 0;
	lazy_clear[k] = 1;
}
 
void up(int k)
{
	ta[k] = ta[l(k)] + ta[r(k)];
	tab[k] = tab[l(k)] + tab[r(k)];
}
 
void down(int k, int l, int r)
{
	int m = l + r >> 1;
	if(lazy_clear[k]) cla(l(k), l, m), cla(r(k), m + 1, r), lazy_clear[k] = 0;
	if(lazya[k]) updateA(l(k), l, m), updateA(r(k), m + 1, r), lazya[k] = 0; 
	if(lazyb[k]) updateB(l(k), l, m), updateB(r(k), m + 1, r), lazyb[k] = 0; 
}
 
void change(int k, int l, int r, int L, int R, int op)
{
	if(L <= l && r <= R)
	{
		if(!op) updateA(k, l, r);
		else if(op == 1) updateB(k, l, r);
		else cla(k, l, r);
		return;
	}
	down(k, l, r);
	int m = l + r >> 1;
	if(L <= m) change(l(k), l, m, L, R, op);
	if(R > m) change(r(k), m + 1, r, L, R, op);
	up(k);
}

int ask(int k, int l, int r, int L, int R)
{
	if(L <= l && r <= R) return tab[k];
	down(k, l, r);
	int m = l + r >> 1, res = 0;
	if(L <= m) res += ask(l(k), l, m, L, R);
	if(R > m) res += ask(r(k), m + 1, r, L, R);
	return res;
}
 
void add(int u, int v, int op)
{
	while(top[u] != top[v])
	{
		if(d[top[u]] < d[top[v]]) swap(u, v);
		if(!op) change(1, 1, n, id[top[u]], id[u], 0);
		else change(1, 1, n, id[top[u]], id[u], 1);
		u = fa[top[u]];
	}
	if(d[u] < d[v]) swap(u, v);
	if(!op) change(1, 1, n, id[v], id[u], 0);
	else change(1, 1, n, id[v], id[u], 1);
}
 
int main()
{
	int T; scanf("%d", &T);
	while(T--)
	{
		scanf("%d%d", &n, &q);
		_for(i, 1, n) g[i].clear(), son[i] = d[i] = top[i] = fa[i] = id[i] = siz[i] = 0;
		cnt = 0;
		_for(i, 2, n)
		{
			int x; scanf("%d", &x);
			g[x].push_back(i);
		}
 
		dfs1(1, 0);
		dfs2(1, 0, 1);
 
		while(q--)
		{
			int na, nb, nc, x;
			scanf("%d%d%d", &na, &nb, &nc);
			while(na--)
			{
				scanf("%d", &x);
				add(1, x, 0);
			}
			while(nb--)
			{
				scanf("%d", &x);
				add(1, x, 1);
			}

			int ans = 0;
			while(nc--)
			{
				scanf("%d", &x);
				ans += ask(1, 1, n, id[x], id[x] + siz[x] - 1);
				change(1, 1, n, id[x], id[x] + siz[x] - 1, 2);
			}
			printf("%d\n", ans);
			cla(1, 1, n);
		}
	}
 
    return 0;
}

DOS Card(线段树)

这题妙啊,用线段树维护多个信息即可

#include <bits/stdc++.h>
#define l(k) (k << 1)
#define r(k) (k << 1 | 1)
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

typedef long long ll;
const int N = 1e5 + 10;
const ll INF = 1e18;

struct node
{
	ll mx1, mx2, mi1, mi2;
	ll zss, ssf, zf, fz, zssf;
}t[N << 2];
int n, m;

node add(node a, node b)
{
	node res;

	vector<ll> ve = {a.mx1, a.mx2, b.mx1, b.mx2};
	sort(ve.begin(), ve.end());
	res.mx1 = ve[3];
	res.mx2 = ve[2];

	ve.clear();
	ve = {a.mi1, a.mi2, b.mi1, b.mi2};
	sort(ve.begin(), ve.end());
	res.mi1 = ve[0];
	res.mi2 = ve[1];

	res.zf = max({a.zf, b.zf, a.mx1 - b.mi1});

	res.fz = max({a.fz, b.fz, -a.mi1 + b.mx1});

	res.zss = max({a.zss, b.zss, a.mx1 + max(b.fz, b.zf), a.zf + b.mx1, 
				a.mx1 + a.mx2 - b.mi1});
	
	res.ssf = max({a.ssf, b.ssf, max(a.zf, a.fz) - b.mi1, 
					a.mx1 - b.mi1 - b.mi2, -a.mi1 + b.zf});
	
	res.zssf = max({a.zssf, b.zssf, a.mx1 + b.ssf, a.zss - b.mi1, a.mx1 + a.mx2 - b.mi1 - b.mi2,
					a.zf + b.zf});
	
	return res;
}

void up(int k)
{
	t[k] = add(t[l(k)], t[r(k)]);
}

void build(int k, int l, int r)
{
	if(l == r)
	{
		ll x; scanf("%lld", &x);
		x = x * x;
		t[k] = {x, -INF, x, INF, -INF, -INF, -INF, -INF, -INF};
		return ;
	}
	
	int m = l + r >> 1;
	build(l(k), l, m);
	build(r(k), m + 1, r);
	up(k);
}

node ask(int k, int l, int r, int L, int R)
{
	if(L <= l && r <= R) return t[k];
	node res = {-INF, -INF, INF, INF, -INF, -INF, -INF, -INF, -INF};
	int m = l + r >> 1;
	if(L <= m) res = add(res, ask(l(k), l, m, L, R));
	if(R > m) res = add(res, ask(r(k), m + 1, r, L, R));
	return res;
}

int main()
{
	int T; scanf("%d", &T);
	while(T--)
	{
		scanf("%d%d", &n, &m);
		build(1, 1, n);

		while(m--)
		{
			int l, r;
			scanf("%d%d", &l, &r);
			node ans = ask(1, 1, n, l, r);
			printf("%lld\n", ans.zssf);
		}
	}

	return 0;
}

F. Stone(博弈 奇偶)

这题结论很简单,题目很复杂……

考虑奇偶,当前是有奇数堆,那么先手可以选一个奇数把所有奇数堆都变成偶数堆。

先手选奇数后,以后所有的数都是奇数了,后手不管怎么选,会把一些奇数堆变成偶数堆

那么先手就把那些奇数堆变成偶数堆即可,比如取s=1

如果当前没有奇数堆,那就把堆数量/=2,那么就变成相同的情况

直到出现奇数堆,那么当前先手可以选的就是小于最小奇数的所有奇数

#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

const int N = 1e6 + 10;
int a[N], n;

int main()
{
	scanf("%d", &n);
	_for(i, 1, n) scanf("%d", &a[i]);

	while(1)
	{
		int mi = 2e9;
		_for(i, 1, n)
			if(a[i] % 2 == 1)
				mi = min(mi, a[i]);
		
		if(mi != 2e9)
		{
			printf("%d\n", (mi + 1) / 2);
			break;
		}

		_for(i, 1, n) a[i] /= 2;
	}

	return 0;
}

周三

P2764 最小路径覆盖问题(网路流)

给一个有向图,问最少有多少条不相交路径把图上的点都覆盖

回顾一下以前的题目

一开始n个点就是n条路,然后一条有向边就可以减少一个答案

根据这个特点,把每个点拆成x和y,然后汇点向x连边,y向汇点连边

原图的边i,j,那就xi向yj连边。边都是容量为1

跑最大流,就是最多合并多少

那么答案就是n-最大流

还有一种等价的建图方式,一个点拆成入点和出点,然后原图的边就是x的出点向y的入点。然后原图的一条有向边只能起一次作用,对应网络中的一条流,所以源点向出点连边,入点向汇点连边.所有边容量都为1

魔术球问题(最小路径覆盖+残量网络)

建边,求最小路径覆盖即可

注意,可以不用二分答案,对于新建立的边,直接加入,然后在残量网络中跑,不用重新跑一次。

[CQOI2015]网络吞吐量(拆点)

首先传输只按照最短路径传输,所以直接跑一遍最短路,把不是最短路上的边删去

最短路上的边满足d[v] = d[u] + w(u, v)

然后这题对点流过的流量有限制,转化成边权,即把点拆成入点出点,中间连点权为容量的边。原图的边容量为正无穷。起点和终点可以看作点权为正无穷

周四

B. Boss Rush(二分答案+状压dp)

看到数据范围往状压dp想

问题出现在处理技能造成的伤害,后来发现如果二分一个时间,时间确定下伤害就可以直接确定。

注意初始化,注意下标

#include<bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++) 
#define _for(i, a, b) for(int i = (a); i <= (b); i++) 
using namespace std;

typedef long long ll;
const int N = 20;
int t[N], len[N], n;
ll k[1 << 18], dp[1 << 18];
vector<ll> s[N]; 
ll H, sum;

bool check(int key)
{
	memset(dp, 0, sizeof dp);
	rep(S, 0, 1 << n)
		_for(i, 1, n)
			if(S & (1 << (i - 1)))
			{
				int S0 = S ^ (1 << (i - 1));
				if(key <= k[S0]) continue;
				dp[S] = max(dp[S], dp[S0] + s[i][min(key - k[S0] - 1, len[i] - 1ll)]);
				if(dp[S] >= H) return true;
			}
	return false;
}

int main()
{
	int T; scanf("%d", &T);
	while(T--)
	{
		scanf("%d%lld", &n, &H);
		_for(i, 1, n) s[i].clear();
		sum = 0;
		_for(i, 1, n)
		{
			scanf("%d%d", &t[i], &len[i]);
			_for(j, 1, len[i])
			{
				int x; scanf("%d", &x);
				if(s[i].size()) s[i].push_back(x + s[i].back());
				else s[i].push_back(x);
				sum += x;
			}
		}

		if(sum < H)
		{
			puts("-1");
			continue;
		}

		rep(S, 0, 1 << n)
		{
			k[S] = -1;
			_for(i, 1, n)
				if(S & (1 << (i - 1)))
					k[S] += t[i];
		}

		int l = -1, r = 3e6;
		while(l + 1 < r)
		{
			int m = l + r >> 1;
			if(check(m)) r = m;
			else l = m;
		}
		printf("%d\n", r);
	}
	
	return 0;
}

L. Two Permutations(dp+优化)

很容易想到一个dp,然后考虑优化

因为是排列,所以可以更新的非常少,直接枚举可以更新的地方即可

注意是否存在用count,不要直接去访问,否则会T

注意取模以及空间大小

#include<bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++) 
#define _for(i, a, b) for(int i = (a); i <= (b); i++) 
using namespace std;

typedef long long ll;
const int N = 3e5 + 10;
const int mod = 998244353; 
int a[N], b[N], c[N << 1], n;

ll id(int i, int j)
{
	return 1LL * i * 1e6 + j;
}

int main()
{
	int T; scanf("%d", &T);
	while(T--)
	{
		unordered_map<ll, int> dp;
		scanf("%d", &n);
		_for(i, 1, n)
		{
			int x; scanf("%d", &x);
			a[x] = i;
		}
		_for(i, 1, n)
		{
			int x; scanf("%d", &x);
			b[x] = i;
		}
		_for(i, 1, 2 * n) scanf("%d", &c[i]);

		dp[id(0, 0)] = 1;
		_for(k, 1, 2 * n)
		{
			int i = a[c[k]];
			int j = k - i;
			if(j >= 0 && dp.count(id(i - 1, j))) (dp[id(i, j)] += dp[id(i - 1, j)]) %= mod;

			j = b[c[k]];
			i = k - j;
			if(i >= 0 && dp.count(id(i, j - 1))) (dp[id(i, j)] += dp[id(i, j - 1)]) %= mod;
		}
		printf("%d\n", dp[id(n, n)]);
	}
	
	return 0;
}

Package Delivery(贪心)

思路比较容易想到,按照右端点排序,然后找到右端点最小的点操作肯定是最优的,然后找可以操作的区间里面右端点最小的。

问题是怎么实现

对于当前的右端点,首先要找到可以操作的区间。那么我们可以把左右端点拆开,然后遇到左端点就加入候选区间中,遇到右区间就操作。

#include<bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++) 
#define _for(i, a, b) for(int i = (a); i <= (b); i++) 
using namespace std;

const int N = 1e5 + 10;
int del[N], l[N], r[N], n, k;

int main()
{
	int T; scanf("%d", &T);
	while(T--)
	{
		vector<pair<int, int>> ve;
		scanf("%d%d", &n, &k);
		_for(i, 1, n)
		{
			scanf("%d%d", &l[i], &r[i]);
			ve.push_back({l[i], -i});
			ve.push_back({r[i], i});
			del[i] = 0;
		}
		sort(ve.begin(), ve.end());

		int ans = 0;
		set<pair<int, int>> s;
		for(auto [pos, id]: ve)
		{
			if(id < 0) s.insert({r[-id], -id});
			else
			{
				if(del[id]) continue; 
				ans++;
				int cnt = k;
				while(cnt && s.size())
				{
					del[s.begin()->second] = 1;
					s.erase(s.begin());
					cnt--;
				}
			}
		}
		printf("%d\n", ans);
	}
	
	return 0;
}

还有一种是队友想的,就是维护当前区间的交区间,当新的区间去前面的交区间没有交集的时候,前面的交区间就必须要操作了,此时将交区间中右端点最小的k个去点即可。

#include<bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++) 
#define _for(i, a, b) for(int i = (a); i <= (b); i++) 
using namespace std;

const int N = 1e6 + 10;
pair<int, int> a[N];
int n, k;

int main()
{
	int T; scanf("%d", &T);
	while(T--)
	{
		scanf("%d%d", &n, &k);
		_for(i, 1, n) scanf("%d%d", &a[i].first, &a[i].second);
		sort(a + 1, a + n + 1);

		int ans = 0;
		priority_queue<int, vector<int>, greater<int>> q;
		_for(i, 1, n)
		{
			while(!q.empty() && q.top() < a[i].first)
			{
				int cnt = k;
				while(cnt && !q.empty()) q.pop(), cnt--;
				ans++;
			}
			q.push(a[i].second);
		}
		printf("%d\n", ans + (q.size() + k - 1) / k);
	}
	
	return 0;
}

K. Taxi(二分)

比较巧妙的二分,不是那么常规

首先没有上限的限制的话就是经典题,直接找xi+yi,xi-yi,-xi-yi,-xi+yi的四个点即可

那么将点按照w排序

取中点wk

比较wk和k~n后缀的不考虑上限的答案d(预处理后缀最值的四个点即可)

如果d <= wk,那么这右半段区间的贡献就是d了,那么用d更新答案,然后舍去右区间继续二分

如果d > wk,这时右半区间的贡献最少都为wk,用这个更新答案,然后1~k都不会产生更优的答案了,舍去左区间,继续二分。

#include<bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++) 
#define _for(i, a, b) for(int i = (a); i <= (b); i++) 
using namespace std;

const int N = 1e5 + 10;
struct node
{
	int x, y, w;
}a[N];
int id[N][4], n, q;

bool cmp(node a, node b)
{
	return a.w < b.w;
}

int cal(node a, node b)
{
	return abs(a.x - b.x) + abs(a.y - b.y);
}

int main()
{
	int T; scanf("%d", &T);
	while(T--)
	{
		scanf("%d%d", &n, &q);
		_for(i, 1, n) scanf("%d%d%d", &a[i].x, &a[i].y, &a[i].w);
		sort(a + 1, a + n + 1, cmp);

		rep(j, 0, 4) id[n][j] = n;
		for(int i = n - 1; i >= 1; i--)
		{
			rep(j, 0, 4) id[i][j] = id[i + 1][j];
			if(a[i].x + a[i].y > a[id[i][0]].x + a[id[i][0]].y) id[i][0] = i;
			if(a[i].x - a[i].y > a[id[i][1]].x - a[id[i][1]].y) id[i][1] = i;
			if(-a[i].x + a[i].y > -a[id[i][2]].x + a[id[i][2]].y) id[i][2] = i;
			if(-a[i].x - a[i].y > -a[id[i][3]].x - a[id[i][3]].y) id[i][3] = i;
		}

		while(q--)
		{
			int x, y;
			scanf("%d%d", &x, &y);

			int ans = 0;
			int l = 1, r = n;
			while(l <= r)
			{
				int m = l + r >> 1, d = 0;
				rep(j, 0, 4) d = max(d, cal(a[id[m][j]], {x, y}));
				if(a[m].w < d) 
				{
					ans = max(ans, a[m].w);
					l = m + 1;
				}
				else
				{
					ans = max(ans, d);
					r = m - 1;
				}
			}
			printf("%d\n", ans);
		}
	}
	
	return 0;
}

周五

[SCOI2007]蜥蜴(建图)

挺容易想到的,一只蜥蜴就是一条流,然后把点权变成边权即可

E. Fox And Dinner(最大流+建图)

这题的关键在于数据范围没有1使得两个数加起来不可能为2,所以质数一定是奇数,所以一定是一奇一偶,那么就是一个二分图

显然可以匹配就连边容量为1,在环中每个点都有两天边,所以入点到左侧点连容量为2的边,右侧点向出点连容量为2的边。

跑最大流,如果等于n就有解。

因为每个环显然是奇偶交替,是偶数,所以奇数一半偶数一半,此时最大流为n。如果不是各一半,那么最大流小于n

最后是输出方案,对于方案就建立边,注意去除反向边和于源点汇点有关的边,容量为0的边显然是流过的边,也就是说两端的点在一个环中。那么建图跑dfs即可

#include <bits/stdc++.h>
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
#define rep(i, a, b) for(int i = (a); i < (b); i++)
using namespace std;
 
typedef long long ll;               
const int N = 300;                  
struct Edge { int from, to, flow; }; 
vector<Edge> edge;
vector<int> g[N];
int d[N], cur[N];                     
int n, m, s, t; 
 
void add(int from, int to, int flow)
{
	edge.push_back(Edge{from, to, flow});
	g[from].push_back(edge.size() - 1);
	edge.push_back(Edge{to, from, 0});
	g[to].push_back(edge.size() - 1);
}
 
bool bfs()
{
	memset(d, 0, sizeof(d));
	queue<int> q;
	q.push(s);
	d[s] = 1;
 
	while(!q.empty())
	{
		int u = q.front(); q.pop();
		for(auto x: g[u])
		{
            Edge e = edge[x];
            if(!d[e.to] && e.flow)     
            {
                d[e.to] = d[u] + 1;
                q.push(e.to);
            }
		}
	}
 
	return d[t];      
}
 
ll dfs(int u, ll in)                           
{
	if(u == t) return in;                     
	ll out = 0;                                
	for(int& i = cur[u]; i < g[u].size(); i++) 
		{
			Edge& e = edge[g[u][i]];          
			if(d[u] + 1 == d[e.to] && e.flow)  
			{
			    ll f = dfs(e.to, min((ll)e.flow, in));
				e.flow -= f;
				edge[g[u][i] ^ 1].flow += f;    
				out += f; in -= f;
				if(in == 0) break;
			}
		}
	return out;
}

const int M = 1e5 + 10;
int vis[M], a[N], k[N], cnt;
vector<int> G[N], res[N], p;

void init()
{
	vis[0] = vis[1] = 1;
	_for(i, 2, 1e5)
	{
		if(!vis[i]) p.push_back(i);
		for(int x: p)
		{
			if(i * x > 1e5) break;
			vis[i * x] = 1;
			if(i % x == 0) break;
		}
	}
}
 
void build()                                   
{
	init();   //不要忘写

    scanf("%d", &n);
	_for(i, 1, n) scanf("%d", &a[i]);
	s = n + 1; t = s + 1;

	vector<int> v1, v2;
	_for(i, 1, n)
	{
		if(a[i] % 2 == 1) v1.push_back(i);
		else v2.push_back(i);
	}
	for(int x: v1) add(s, x, 2);
	for(int x: v2) add(x, t, 2);
	for(int x: v1)
		for(int y: v2)
			if(!vis[a[x] + a[y]])
				add(x, y, 1);
}

void dfs2(int u, int fa)
{
	k[u] = 1;
	res[cnt].push_back(u);
	for(int v: G[u])
	{
		if(v == fa || k[v]) continue;
		dfs2(v, u);
	}
}
 
int main()
{
    build();
 
	ll ans = 0;
	while(bfs())
	{
	    memset(cur, 0, sizeof(cur));
        ans += dfs(s, 1e18);
	}
	
	if(ans != n) 
	{
		puts("Impossible");
		return 0;
	}

	for(auto [u, v, flow]: edge)
		if(u != s && v != t && !flow && a[u] % 2 == 1 && a[v] % 2 == 0) //注意去除反向边
		{
			G[u].push_back(v);
			G[v].push_back(u);
		}
	
	_for(i, 1, n)
		if(!k[i])
		{
			cnt++;
			dfs2(i, 0);
		}
	
	printf("%d\n", cnt);
	_for(i, 1, cnt)
	{
		printf("%d ", res[i].size());
		for(int x: res[i]) printf("%d ", x);
		puts("");
	}
 
	return 0;
}

Dining(网络流)

模仿二分图匹配,注意把牛放在中间,这样保证了饮料和食物都用了一次,但是没有保证牛用了一次,那么拆点中间连一个容量为1的边即可。

星际转移问题(分层思想+网络流)

这题最关键的一点在于分层,每一天一层

然后根据太空船在层与层之间连边,还要加入留在原地的边

如果固定了源点汇点,就可以加边然后在残量网络上继续跑,所以我们固定汇点,让每一层的终点连向一个超级汇点,每次加一层,在残量网络上跑最大流。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值