长春集训 Day 8 - 模拟赛、DP 专题

长春集训 Day 8 - 模拟赛、DP 专题

模拟赛 100+0+0+0,打暴力的能力还得增强

T1 - 画圈

Description

给定 \(n\) 个点,\(m\) 条边的连通简单无向图,每条边初始为黑色或白色。可以进行若干次如下操作:

  • 选择图中至少包含一条白色边的简单环,并把这个环上的所有边都染成黑色。

可以在任意时刻停止操作,不要求图中所有边最终都被染成黑色。求最多可以执行多少次操作。

多测,\(1 \le T \le 10^4,\ 1 \le n,m,\sum n,\sum m \le 3 \times 10^5\)

Solution

注意到每次只染黑一个白边一定最优。可以构造出一些数据,使得其中有“不得不染黑多条白边”的情况,这种情况下,每多染黑一条白边,一定会把两个黑边构成的连通块合并。所以记黑边构成的连通块有 \(b\) 个,那么最多合并 \(b-1\) 次。记白边数量为 \(a\),则答案为 \(a - (b-1)\)

另解 对图中所有黑边跑出生成树森林,然后添加若干白边使它们连通,构建出原图白边数最少的最小生成树,与上式等价。

Code

#include <bits/stdc++.h>
// #pragma GCC optimize(2)
#define int long long
#define inf 1e18
#define debug cout << '!';
#define filein(x) freopen(#x".in", "r", stdin);
#define fileout(x) freopen(#x".out", "w", stdout);
#define file(x) filein(x) fileout(x)
using namespace std;
const int N = 2e5+5;
int T, n, m;
bool flg[N];
struct DSU {
	int fa[N];
	void init() {
		for (int i = 1; i <= n; i++) {
			flg[i] = 0;
			fa[i] = i;
		}
	}
	int find(int x) {
		if (x == fa[x]) return x;
		return fa[x] = find(fa[x]);
	}
	void unite(int x, int y) {
		fa[find(x)] = find(y);
	}
} dsu;
void solve() {
	cin >> n >> m;
	dsu.init();
	int cnt = 0, tot = 0;
	for (int i = 1; i <= m; i++) {
		int u, v, w; cin >> u >> v >> w;
		if (w == 0) {
			cnt++;
		} else {
			dsu.unite(u, v);
		}
	}
	for (int i = 1; i <= n; i++) {
		flg[dsu.find(i)] = 1;
	}
	for (int i = 1; i <= n; i++) {
		if (flg[i] == 1) tot++;
	}
	cout << cnt - tot + 1 << '\n';
}
signed main() {
	cin.tie(0) -> sync_with_stdio(0);
	cin >> T;
	while (T--) {
		solve();
	}
	return 0;
}

T3 - No explanation

Description

给定 \(n\) 个只包含小写字母的字符串数组 \(s\),要求从中选出一些字符串,并选择一个合适的顺序将它们首位拼接成一个新字符串 \(S\),且保证 \(S\) 中不存在子序列 \(\texttt{luolikong}\)(无需连续)。请计算 \(S\) 的最大长度。

\(1 \le n \le 1000, 1 \le |s_i|, \sum |s_i| \le 10^5\)

Solution

\(t = \texttt{luolikong}\)。由于我们只关心是否会出现子序列 \(t\),因此可以建立有限状态自动机,探明状态之中的转移关系。

定义状态 \(j(0 \le j \le 9)\) 表示目前已经匹配了 \(t\) 的前 \(j\) 个字符,其中 \(j=9\) 表示全部匹配,为非法状态。

每添加一个字符串都会产生一个转移关系,因此定义 \(f_{i,j}\) 表示当前匹配状态为 \(j\) 时,从头到尾读入字符串 \(s_i\) 后,匹配状态会变为什么。可以用双指针计算 \(f\) 数组的值。

另外,每拼接一个字符串都会产生大于或等于 \(0\) 的贡献,因此状态转移是单调不降的。

当把若干个字符串按某种顺序拼接时,等价于在 \(0 \to 8\) 上走一条路径。

对于能使状态前进一步的字符串,我们称之为“前向字符串“;对于使得状态在某点不动的字符串,我们称之为”自环字符串“。可能的状态只有 \(2^8\) 种(第一个字符串一定是前向字符串)。

假设固定状态集 \(X = \{x_0, x_1, \cdots, x_k\}\),现有 \(k\) 条连边 \(x_{j-1} \to x_j\) 需要满足。考虑压位。定义 \(st\) 表示一个二进制状态集,其中第 \(j\) 位表示边 \(x_{j} \to x_{j+1}\) 是否被字符串覆盖。\(dp_{st}\) 表示覆盖 \(st\) 中的连接边的前提下,能获得的最大拼接总长度。

对于每个字符串,只能使用一次,因此类似于一个 01 背包状物,我们可以定义两个 \(dp\) 数组来滚动更新,具体地,我们有如下转移方式:

  • 不选该字符串

    \[dp'_{st} = \max(dp'_{st}, dp_{st}) \]

  • 作为自环字符串而加入

    如果 \(\exist j \in [0,k]\),使得 \(f_{i,x_j} = x_j\),那么该字符串即为自环。自环不改变状态值,因此有

    \[dp'_{st} = \max(dp'_{st}, dp_{st} + len) \]

  • 作为前向字符串而加入

    如果 \(\exist j\in[0,k]\),使得 \(st_j = 0 \land f_{j,x_j} = x_{j+1}\),那么可以更新状态并转移

    \[dp'_{st \cup \{j\}} = \max(dp'_{st \cup \{j\}}, dp_{st} + len) \]

最终,如果当前固定的状态集能够覆盖 \(k\) 条连边,即 \(dp_{2^k-1} > -\infin\),则方案合法,更新全局答案。

Code

#include <bits/stdc++.h>
// #pragma GCC optimize(2)
#define int long long
#define inf 1e18
#define debug cout << '!';
#define filein(x) freopen(#x".in", "r", stdin);
#define fileout(x) freopen(#x".out", "w", stdout);
#define file(x) filein(x) fileout(x)
using namespace std;
const int N = 1005, M = 2005, L = 9;
const string t = "luolikong";
int n;
string s[N];
int nxt[N][L], ans;
int dp[M][2];
signed main() {
	cin.tie(0) -> sync_with_stdio(0);
	cin >> n;
	for (int i = 1; i <= n; i++) {
		cin >> s[i];
	}
	for (int i = 1; i <= n; i++) {
		for (int st = 0; st < 9; st++) {
			int cur = st;
			for (auto c : s[i]) {
				if (c == t[cur] and cur < 9) {
					cur++;
					if (cur == 9) break;
				}
			}
			nxt[i-1][st] = cur;
		}
	}
	for (int st0 = 1; st0 < (1<<L); st0 += 2) {
		vector<int> dgt;
		for (int i = 0; i < 9; i++) {
			if ((st0 >> i) & 1) {
				dgt.push_back(i);
			}
		}
		int k = dgt.size() - 1;
		for (int st1 = 0; st1 < (1<<k); st1++) {
			dp[st1][1] = dp[st1][0] = -inf;
		}
		dp[0][0] = 0;
		for (int i = 1; i <= n; i++) {
			if (nxt[i-1][0] == 9) continue;
			int len = s[i].size();
			for (int st1 = 0; st1 < (1<<k); st1++) {
				dp[st1][1] = dp[st1][0];
			}
			for (int st1 = 0; st1 < (1<<k); st1++) {
				if (dp[st1][0] <= -inf/2) continue;
				for (int j = 0; j <= k; j++) {
					if (dgt[j] == nxt[i-1][dgt[j]]) {
						dp[st1][1] = max(dp[st1][1], dp[st1][0] + len);
					}
				}
				for (int j = 0; j < k; j++) {
					if (st1 & (1<<j)) continue;
					if (dgt[j+1] == nxt[i-1][dgt[j]]) {
						dp[st1|(1<<j)][1] = max(dp[st1|(1<<j)][1], dp[st1][0] + len);
					}
				}
			}
			for (int st1 = 0; st1 < (1<<k); st1++) {
				dp[st1][0] = dp[st1][1];
			}
		}
		if (dp[(1<<k)-1][0] > -inf/2) {
			ans = max(ans, dp[(1<<k)-1][0]);
		}
	}
	cout << ans;
	return 0;
}
posted @ 2026-08-09 22:18  L-Coding  阅读(4)  评论(0)    收藏  举报