AtCoder Beginner Contest 214

AtCoder Beginner Contest 214

赛时只切了 A~F,看了官方题解后发现 G,H 确实神仙。

拥有良好的参赛体验(和上大分)

\(A\sim D\)

A,B 就按题意模拟。

C 比赛时卡了一会,发现可以将 \(T\) 最小的点作为起点把环断开,然后 \(Ans_i=\min(Ans_{i-1}+S_{i-1},T_i)\)

D 逆向思维将删边变为加边,并查集维护一下即可。

\(E\)

给定 \(n\) 个球,每个求可以放入 \(L_i\sim R_i\) 号盒子中的某一个,每个盒子至多放一个球。

问是否存在方案将所有的求放入盒子中。

将所有线段按照 \(R\) 升序作为第一关键字,\(L\) 升序作为第二关键字。

利用并查集维护每个点后面第一个空缺的点,从而得到一个球能否放入,放入应当尽量靠左。

这样贪心的正确性显然,因为 \(R\) 小的尽量靠左填,对 \(R\)​ 较大的影响就达到最小。

由于值域较大(\(V=10^9\)),所以需要用 map 记录并查集信息,总时间复杂度 \(O(n\alpha(n)\log V)\)

#include<cstdio>
#include<cstring>
#include<algorithm>
#include<map>
using namespace std;
 
const int N = 2e5 + 10;
int T, n;
struct Node{int l, r;} a[N];
map<int, int> fa;
 
int read(){
	int x = 0, f = 1; char c = getchar();
	while(c < '0' || c > '9') f = (c == '-') ? -1 : 1, c = getchar();
	while(c >= '0' && c <= '9') x = x * 10 + c - 48, c = getchar();
	return x * f;
}
 
bool cmp(Node a, Node b) {return (a.r == b.r ? a.l < b.l : a.r < b.r);}
 
int Get(int x){
	if(fa.find(x) == fa.end()) return x;
	if(x == fa[x]) return x;
	return fa[x] = Get(fa[x]);
}
 
void Work(){
	fa.clear(); n = read();
	for(int i = 1; i <= n; i ++) a[i].l = read(), a[i].r = read();
	sort(a + 1, a + n + 1, cmp);
	bool flag = true;
	for(int i = 1; i <= n; i ++){
		int p = Get(a[i].l);
		if(p <= a[i].r)
			fa[p] = Get(p + 1);
		else
			{flag = false; break;}
	}
	if(flag) puts("Yes"); else puts("No");
}
 
int main(){
	T = read();
	while(T --) Work();
	return 0;
}

官方正解,从小到大枚举左端点,然后将所有同一左端点的右端点插入小根堆,并用一个指针标记当前填到的位。

若堆顶的右端点 \(<\) 当前位那么无解,否则将指针右移并弹出堆顶。发现这和上面的贪心思路是相似的。

但是时间是 \(O(n\log n)\)​ 的,但是上面的写法很简洁,在不考虑时间效率的情况下性价比更高。

\(F\)

给定一个字符串,可以保留不相邻的字符,求最终得到本质不同字符串的个数。

\(f(i)\) 表示第 \(i\) 位为结尾的字符串个数,\(g(i)\) 表示前 \(i\) 位能表出的字符串个数。

\(pre(i)\) 表示前面最近的与 \(i\) 相同字符的位置,有方程:

\[f(i)=g(i-2)-g(pre(i)-2)+Valid(i) \]

\[g(i)=g(i-1)+f(i) \]

\(Valid(i)\)\(0/1\),当且仅当 \(i\) 是这个字符第一个出现的位置时为 \(1\)

于是就完美 \(O(n)\) 解决了。

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
 
typedef long long LL;
const LL P = 1e9 + 7;
const int N = 2e5 + 10;
int n, pos[30], pre[N];
LL f[N], g[N]; char str[N];
 
int main(){
	scanf("%s", str + 1); n = strlen(str + 1);
	for(int i = 1; i <= n; i ++)
		pre[i] = pos[str[i] - 'a'], pos[str[i] - 'a'] = i;
	for(int i = 1; i <= n; i ++){
		if(! pre[i]) {
			f[i] = 1;
			if(i > 2) f[i] = (f[i] + g[i - 2]) % P;
		}
		else{
			if(i > 2) f[i] = g[i - 2];
			if(pre[i] > 2) f[i] = (f[i] - g[pre[i] - 2] + P) % P;
		}
		g[i] = (g[i - 1] + f[i]) % P;
	}
	printf("%lld\n", g[n]);
	return 0;
}

\(G\)

给定两个 \(n\) 的排列 \(A,B\)​,求 \(n\)​ 的排列中满足 \(r_i\neq a_i\)​ 且 \(r_i\neq b_i\)​ 的个数。

个人认为比 \(H\) 更具有思维性。

考虑容斥,设 \(h(i)\) 表示有 \(i\) 个位的 \(r_i=a_i\)\(r_i=b_i\),那么:

\[Ans=\sum\limits_{i=0}^n (-1)^ih(i)(n-i)! \]

现在需要处理出 \(h\) 数组,观察排列的性质,不难发现,如果所有 \((a_i,b_i)\) 连边,那么图由多个环组成。

因为每个点的度数都为 \(2\),将边当做是填的位置,那么每个点可以选择与连接它的边匹配。

匹配就以为这这个位置填了 \(a_i/b_i\),当然,每个点只能匹配一条边,反之亦然。

那么每个环都是等价的子问题,设 \(g(i,j)\) 表示在长度为 \(i\) 的环里有 \(j\) 对点边匹配的方案数。

提前处理出所有环的长度,假设共有 \(t\) 个环,第 \(i\) 个环的长度为 \(c_i\)​,枚举每一个 \(h(j)\),那么:

\[h(i,j)=\sum\limits_{k=0}^n h(i-1,j-k)\times g(c_i,k) \]

当然第一维可以滚动数组滚掉,最终的 \(h(i)\) 等价于上式的 \(h(n,i)\)​​。

问题变为求解所有 \(g(i,j)\),设 \(f(i,j,0/1,0/1)\) 表示长度为 \(i\)​ 的环,有 \(j\) 对点边配对。

我们将环看做是序列(\(i\) 个点 \(i-1\) 条边)从末端到初端连一条边 \((i,1)\)​。

那么第一个 \(0/1\) 表示点 \(i\) 是否与边 \((i-1,i)\) 匹配,第二个表示点 \(i\) 是否与 \((i,1)\) 匹配。

每次向 \(i+1\) 拓展相当于考虑新加一个点和一条边,对于 \(f(i,j,s,t)\) 可以有三种转移:

  1. \(f(i+1,j,0,t)\),即第 \(i+1\) 个点不与新加的边匹配。
  2. \(f(i+1,j+1,1,t)\),即第 \(i+1\) 个点与新加的边匹配。
  3. \(f(i+1,j+1,0,t)\),仅当 \(s=0\) 的时候可行,即让第 \(i\)​ 个点与新加的边匹配。

显然 \(f(i,j,s,t)\)\(s,t\) 不能同时为 \(1\),因为末端点不能同时与两条边匹配,所以有:

\[g(i,j)=f(i,j,0,0)+f(i,j,0,1)+f(i,j,1,0) \]

于是问题完美解决,时间复杂度 \(O(n^2)\)

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
 
typedef long long LL;
const int N = 3010;
const LL MOD = 1e9 + 7;
int n, a[N], b[N], to[N], fac[N];
int h[N], _h[N], g[N][N], f[N][N][2][2];
bool vis[N];
 
int read(){
	int x = 0, f = 1; char c = getchar();
	while(c < '0' || c > '9') f = (c == '-') ? -1 : 1, c = getchar();
	while(c >= '0' && c <= '9') x = x * 10 + c - 48, c = getchar();
	return x * f;
}
 
void P(int &x, int y) {x = (1LL * x + y + MOD) % MOD;}
 
int main(){
	n = read();
	for(int i = 1; i <= n; i ++) a[i] = read(), to[a[i]] = i;
	for(int i = 1; i <= n; i ++) b[i] = read();
	f[1][0][0][0] = 1;
	f[1][1][0][1] = 1;
	f[1][1][1][0] = 1;
	for(int i = 1; i <  n; i ++)
		for(int j = 0; j <= i; j ++)
			for(int s = 0; s < 2; s ++)
				for(int t = 0, v; t < 2; t ++) if(v = f[i][j][s][t]){
					P(f[i + 1][j][0][t], v);
					P(f[i + 1][j + 1][1][t], v);
					if(!s) P(f[i + 1][j + 1][0][t], v);
				}
	g[1][0] = g[1][1] = 1;
	for(int i = 2; i <= n; i ++)
		for(int j = 0; j <= i; j ++)
			for(int s = 0; s < 2; s ++)
				for(int t = 0; t < 2; t ++)
					if(s + t <= 1) P(g[i][j], f[i][j][s][t]);
	h[0] = 1;
	for(int i = 1; i <= n; i ++) if(!vis[i]){
		int cnt = 0;
		for(int u = i; !vis[u]; u = to[b[u]]){
			vis[u] = true;
			cnt ++;
		}
		for(int j = 0; j <= n; j ++) _h[j] = 0;
		for(int j = 0; j <= n; j ++)
			for(int k = 0; k <= cnt; k ++)
				if(j + k <= n) P(_h[j + k], 1LL * h[j] * g[cnt][k] % MOD);
		for(int j = 0; j <= n; j ++) h[j] = _h[j];
	}
	fac[0] = 1;
	for(int i = 1; i <= n; i ++) fac[i] = 1LL * fac[i - 1] * i % MOD;
	int ans = 0;
	for(int i = 0; i <= n; i ++)
		if(i & 1)P(ans, - 1LL * fac[n - i] * h[i] % MOD);
		else     P(ans,   1LL * fac[n - i] * h[i] % MOD);
	printf("%d\n", ans);
	return 0;
}

\(H\)

给定有向图,求用从 \(1\) 出发的 \(K\) 条路径覆盖图形能得到的最大点权和。(重复覆盖只算一次)

一个强联通分量显然可以一次搞定,所以直接缩点变成 DAG。

然后比较套路的费用流模型,将每个点拆为两个点 \(a_i,b_i\) 得到拆点二分图,连边:

  1. 连接 \((S,a_1,\inf,0)\)
  2. 对于所有 \(b\),连接 \((b_i,T,\inf,0)\)
  3. 对于所有点,连接 \((a_i,b_i,1,val_i)\),以及 \((a_i,b_i,\inf,0)\)
  4. 对于图中有的边 \((u,v)\),连接 \((b_u,a_v,\inf,0)\)​。

这里 \(\inf\) 可取 \(K\),跑最大费用最大流即可,但是点数达到 \(10^5\) 级别直接 EK + spfa 会完美去世。

  • 优化一,将费用取负变为最小费用最大流,这样就可以跑最短路。

然后利用 Dijkstra 代替 spfa,方式是得到势能函数 \(h(u)\) 保证所有权非负,具体戳这里

  • 优化二,初始势能函数的统计方法是提前跑一遍 spfa,但可以避免该算法。

为了完全避免已死算法,可以充分利用拆点二分图的性质。

令强联通分量的编号为逆拓扑序,每个强联通分量拆的点编号相邻且逐渐增大,\(S=0\) 最小,\(T=2n+1\) 最大。

那么边只会从编号小的点连到编号大的点,那么就可以真正 \(O(n+m)\) 转移了。

  • 优化三,只有与 \(1\) 联通的强联通分量需要考虑,所以只需要调用 tarjan(1) 即可。
  • 优化四,用 vector 代替费用流中的大部分数组,然后 resize(T+1)

这个算卡常了,因为普通大数组的寻址时间真的太长了,实测极限数据直接 4000ms 变为 800ms

#include<cstdio>
#include<cstring>
#include<algorithm>
#include<vector>
#include<queue>
using namespace std;
 
typedef long long LL;
const int N = 2e5 + 10;
const LL INF = 1e18;
 
int n, m, k, S, T, t, cnt = 1, a[N]; LL ans, val[N];
int tot, top, num, dfn[N], low[N], stk[N], col[N];
struct Edge{int nxt, to, val; LL cost;} ed[N << 3];
vector<int> G[N], head, incf, pre;
vector<LL> d, h;
vector<bool> vis;
 
int read(){
	int x = 0, f = 1; char c = getchar();
	while(c < '0' || c > '9') f = (c == '-') ? -1 : 1, c = getchar();
	while(c >= '0' && c <= '9') x = x * 10 + c - 48, c = getchar();
	return x * f;
}
 
void tarjan(int u){
	dfn[u] = low[u] = ++ num;
	stk[++ top] = u;
	for(int i = 0; i < (int) G[u].size(); i ++){
		int v = G[u][i];
		if(!dfn[v])
			tarjan(v), low[u] = min(low[u], low[v]);
		else if(!col[v])
			low[u] = min(low[u], dfn[v]);
	}
	if(dfn[u] == low[u]){
		int v; t ++;
		do{
			v = stk[top --];
			col[v] = t;
		} while(v != u);
	}
}
 
void add(int u, int v, int w, LL c){
	ed[++ cnt] = (Edge){head[u], v, w,  c}; head[u] = cnt;
	ed[++ cnt] = (Edge){head[v], u, 0, -c}; head[v] = cnt;
}
 
bool dijkstra(){
	priority_queue<pair<LL, int> > q;
	for(int i = S; i <= T; i ++) d[i] = INF, vis[i] = false;
	d[S] = 0, incf[S] = k, q.push(make_pair(0, S));
	while(!q.empty()){
		int u = q.top().second; q.pop();
		if(vis[u]) continue;
		vis[u] = true;
		for(int i = head[u]; i; i = ed[i].nxt){
			int v = ed[i].to, w = ed[i].val;
			LL c = ed[i].cost + h[u] - h[v];
			if(w && d[v] > d[u] + c){
				d[v] = d[u] + c;
				incf[v] = min(w, incf[u]);
				pre[v] = i;
				q.push(make_pair(- d[v], v));
			}
		}
	}
	return d[T] != INF;
}
 
void update(){
	for(int i = S; i <= T; i ++) h[i] += d[i];
	int u = T;
	while(u != S){
		int i = pre[u];
		ed[i    ].val -= incf[T];
		ed[i ^ 1].val += incf[T];
		u = ed[i ^ 1].to;
	}
	ans += h[T] * incf[T];
}
 
int main(){
	n = read(), m = read(), k = read();
	for(int i = 1; i <= m; i ++) {
		int u = read(), v = read();
		G[u].push_back(v);
	}
	for(int i = 1; i <= n; i ++) a[i] = read();
	tarjan(1);
	for(int i = 1; i <= n; i ++)
		if(col[i]) {
			col[i] = t - col[i] + 1;// 注意这里用的是逆拓扑序作为编号。
			val[col[i]] += a[i];
		}
	int SZ = 2 * t + 2;
	head.resize(SZ), incf.resize(SZ), pre.resize(SZ);
    d.resize(SZ), h.resize(SZ), vis.resize(SZ);
	S = 0, T = 2 * t + 1;
	add(S, 1, k, 0);
	for(int i = 1; i <= t; i ++){
		add(2 * i - 1, 2 * i, 1, - val[i]);
		add(2 * i - 1, 2 * i, k, 0);
		add(2 * i, T, k, 0);
	}
	for(int u = 1; u <= n; u ++) if(col[u])
		for(int i = 0; i < (int) G[u].size(); i ++){
			int v = G[u][i];
			if(col[u] != col[v]) add(2 * col[u], 2 * col[v] - 1, k, 0);
		}
	for(int u = S; u <= T; u ++)
		for(int i = head[u]; i; i = ed[i].nxt){
			int v = ed[i].to;
			if(v > u) h[v] = min(h[v], h[u] + ed[i].cost);
		}
	while(dijkstra()) update();
	printf("%lld\n", - ans);
	return 0;
}
posted @ 2021-08-16 23:07  LPF'sBlog  阅读(74)  评论(0编辑  收藏  举报