DP 好题记录

一些想法

  1. \(\rm DP\) 优化:

    • 基于题目中性质减少有用的状态,使得状态变少。

    • 基于已有模型的思想提高转移效率。

    • 基于拆贡献的方法使用数据结构维护

    • 基于类似性质使用常见 \(\rm DP\) 优化方法优化转移(如决策单调性,斜率优化,\(\rm DP\) 凸优化等)

    • 基于继承方式减少对不更改状态的时间开销(一般使用队列代替数组赋值)

    • 基于找规律(如拉插)优化需要的空间。

  2. 各类 \(\rm DP\) 设计常见方法:

核心是通过某些特殊结构来刻画答案的形态,从而通过记录必要信息的方法来划分子结构进行 \(\rm DP\)

  • 最优化:

    • 贪心刻画 \(\rm Pattern\),调整掉不可能的形态,得出某些特殊形态

    • 通过 \(\sum, ||, \min, \max\) 互相转化完成子问题的规约。

    • 通过调整条件辅助刻画 \(\rm DP\) 过程。

    • 使用调整法删去没必要考虑的元素

  • 计数:

    • 考虑如何判定,寻找充要条件

    • 通过某些方式刻画判定过程,对这个过程 \(\rm DP\)

    • 通过构造答案到某些结构的单射,对结构进行划分并计数。

    • 考虑常见字眼(如 “恰好” “至少” “钦定”) 之间的转化(容斥,二项式反演),对一个更简单的问题计数。

Armor and Weapons

\(f_{i, j}\) 是盔甲 \(i\),武器 \(j\) 时的最少时间,转移是平凡的。但是 \(O(n^2)\) 显然超时。

注意答案不会太大,因为当没有加成时,在 \(i\) 轮的情况下最多可以得到 \(Fib_{i + 2}\) 的盔甲或武器。由此可知答案大概在 \(O(\log n)\) 的量级。

于是我们考虑换量:设 \(f_{i, j}\) 是在通过 \(i\) 轮,盔甲为 \(j\) 时,我们最大能拿到什么武器。

初始化 \(f_{1, 1} = 1\)。转移容易。

qwq
#include<bits/stdc++.h>
#define int long long 
using namespace std; 

const int N = 2e5 + 10;

int n, m, t;
map<pair<int, int>, int> EX;
int f[N], g[N];

int val(int i){
	int res = i + g[i];
	if(EX.count({i, g[i]})) res++;
	return res; 
}

signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0); cout.tie(0);
	bool swp = false;
	cin >> n >> m >> t; if(n > m) swap(n, m), swp = true;
	for(int i = 1; i <= t; i++){
		int x, y; cin >> x >> y; if(swp) swap(x, y);
		EX[make_pair(x, y)] = 1;
	}
	int ans = 0; g[1] = 1;
	if(n == 1 && m == 1){cout << 0; return 0;}
	while(++ans){
		for(int i = 1; i <= n; i++) if(g[i]) f[i] = max(f[i], min(m, val(i))), f[min(n, val(i))] = max(f[min(n, val(i))], g[i]); 
		for(int i = n; i; i--) g[i] = max(g[i + 1], f[i]), f[i] = 0;
//		cout << g[n] << "\n";
		if(g[n] >= m){cout << ans; return 0;} 
	}

	return 0;
}

Formalism for Formalism

首先考虑转化问题,我们称一个等价集合是一个集合元素全部等价的集合,问题转化成求集合数量。

但这样还不好做,于是我们考虑用一个集合中字典序最小的那个字符串来代替该集合,这是常见的 \(\rm Trick\)

考虑从前往后填数,现在考虑第 \(i\) 位填什么数。由于我们填出来的数有字典序最小的限制,在某些状态下,有一些数我们可能是不能填的,但是我们并不知道我们可以填些什么数。

注意到可能填的数只有 \(10\) 种,我们可以直接将可以填的数加入到状态表示中。自然的,我们令 \(f_{i, S}\) 为当填到第 \(i\) 位,下一位填数可行状态集表示为 \(S\) 时,最大数组长度为多少。

找到 \(f_{i, S}\) 的前驱是困难的,于是我们考虑 “我为人人” 的刷表法。对于一个 \(f_{i - 1, S}\) 这样的已知状态,我们用它来为 \(f_{i, S0}\) 这样的状态做贡献。我们枚举第 \(i\) 位填的数,现在来计算 \(S0\)。容易发现,第 \(i + 1\) 位不可填数 \(j\),当且仅当 \(j\)\(i\) 可以交换且 \(j < i\)

于是我们预处理出每种状态的后继即可。

qwq
#include<bits/stdc++.h>
#define int long long
using namespace std;
 
const int N = 1e5 + 10, V = 10, mod = 998244353;
int n, m, nxt[(1 << V + 2)][V + 2], g[(1 << V + 2)], f[(1 << V + 2)];
bool con[V + 2][V + 2];
 
signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0); cout.tie(0);
	cin >> n >> m;
	for(int i = 1; i <= m; i++){
		int x, y; cin >> x >> y;
		con[x][y] = con[y][x] = true;
	}
	g[0] = 1;
	for(int S = 0; S < (1 << V); S++){
		for(int i = 0; i < V; i++){
			if(S & (1 << i)){nxt[S][i] = -1; continue;}
			for(int j = 0; j < V; j++) if(con[i][j] && (j < i || (S & (1 << j)))) nxt[S][i] |= (1 << j);
		}
	}
	for(int i = 1; i <= n; i++){
		for(int S = 0; S < (1 << V); S++){
			if(!g[S]) continue; //这个优化很重要!
			for(int ths = 0; ths < V; ths++){
				if(nxt[S][ths] != -1) f[nxt[S][ths]] = (f[nxt[S][ths]] + g[S]) % mod;
			}
		}
		for(int i = 0; i < (1 << V); i++) g[i] = f[i], f[i] = 0;
	}
	int ans = 0;		
	for(int i = 0; i < (1 << V); i++) ans = (ans + g[i]) % mod;
	cout << ans;
 
// 	system("pause");
	return 0;
}

P3643 [APIO2016] 划艇

首先容易设计出朴素 DP:设 \(f_{i, j}\) 为考虑前 \(i\) 个数,且 \(a_i = j\) 时的方案数。

但是 \(j\) 可能很大,于是考虑优化状态个数。

如何优化呢?注意到题目中数值上限有 \(10^9\),但是出现的数值只有 \(2n\) 个,进而我们考虑离散化。注意,我们为了方便处理,将每个 \(r_i\) 都加一,再进行离散化。

令第 \(i\) 小的数对应原来的数是 \(rk_i\),不妨设有 \(m\) 个不同的数。显然的,我们将 \(rk_1 ~ rk_m\) 分为了 \(m - 1\) 个互不相交的区间。注意到一个原来的区间一定可以通过几个相邻的区间组合而成,从而可以优化状态个数:

\(f_{i, j}\) 为考虑前 \(i\) 个数,且 \(a_i \in [rk_j, rk_{j + 1})\) 的方案数。于是我们枚举它的前驱 \(lst\),并且让 \([lst + 1, i]\) 区间中的任意 \(a_k\)\(\in [rk_j, rk{j + 1})\) 或等于 \(0\)(我们称不选为令 \(a_k = 0\))。

但是区间中可能会有一些 \([l_k, r_k] \notin [rk_j, rk_{j + 1})\),我们只能强制 \(a_k = 0\),这些不需要考虑。我们只考虑剩下的区间,不妨设剩下的区间有 \(cnt\) 个。(接下来区间长度 \(len = rk_{j + 1 - rk_j}\)

单独考虑是困难的,于是我们考虑对它们整体考虑。首先分析没有 \(0\) 的情况,可以发现如果我们选了 \(len\) 个不同数中的 \(i - lst\) 个,序列排列方式将是唯一的,所以答案是 \(C_{len, i - lst + 1}\)。接下来在序列中加入 \(i - lst\) 个零(因为 \(a_i\) 不能为 \(0\))。对他们进行标号 \(1....i-lst\),对于一个标号为 \(i\)\(0\),我们将它放置在第 \(i\) 个位置。于是其实对于一个不同的选择集合,不难发现都有唯一的方式,于是有 \(C_{len + cnt - 1}^{cnt}\) 放置方式。

从而我们可以得出状态转移方程:

\[f_{i, j} = \sum^{i-1}_{lst = 0}{\sum^{j - 1}_{j' = 0} f_{lst, j'} \times C_{len + cnt - 1}^{cnt}} \]

初始化 \(f_{0, 0} = 1\),答案 \(\sum_{i = 1} ^ n {f_{i, m - 1}}\)

但这样是 \(O(n^4)\) 的,容易发现可以用前缀和优化。具体的,我们令 \(s_{i, j} = \sum_{j' = 0}^j {f_{i, j'}}\)。状态转移方程就为 \(f_{i, j} = \sum_{lst = 1}^i s_{lst, j - 1} \times C_{len + cnt - 1}^{cnt}\)。但是这样又有一个问题,C 的计算不是容易的。注意到 \(C(x, y) = C(x - 1, y - 1) \cdot x \cdot inv(y)\),而且当 \(cnt = 1\) 时,这是容易计算的,于是我们可以边计算 \(f\) 边更新 \(C\) 的值。

qwq
#include<bits/stdc++.h>
#define int long long
using namespace std;

const int N = 500 + 10, mod = 1e9 + 7;

int n, m, f[N][2 * N], l[N], r[N], rk[2 * N], s[N][2 * N], C[N];

int qpow(int x, int y){
	int ret = 1;
	while(y){
		if(y & 1) ret = ret * x % mod;
		x = x * x % mod;
		y >>= 1;
	}
	return ret;
}

signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0); cout.tie(0);
	cin >> n;
	for(int i = 1; i <= n; i++) cin >> l[i] >> r[i], r[i]++, rk[i] = l[i], rk[i + n] = r[i];
	sort(rk + 1, rk + 2 * n + 1); m = unique(rk + 1, rk + 2 * n + 1) - (rk + 1);
	for(int i = 1; i <= n; i++) l[i] = lower_bound(rk + 1, rk + m + 1, l[i]) - rk, r[i] = lower_bound(rk + 1, rk + m + 1, r[i]) - rk;	
	for(int i = 0; i < m; i++) s[0][i] = 1;
	for(int j = 1; j < m; j++){
		int len = rk[j + 1] - rk[j], tot = 0; C[0] = len;
		for(int i = 1; i <= n; i++) C[i] = (C[i - 1] * (i + len) % mod) * qpow(i + 1, mod - 2) % mod;
		for(int i = 1; i <= n; i++){
			tot = 0;
			if(l[i] <= j && r[i] > j){
				for(int lst = i - 1; lst >= 0; lst--){
					f[i][j] = (f[i][j] + s[lst][j - 1] * C[tot] % mod) % mod;
					if(l[lst] <= j && r[lst] > j) tot++;
				} 
			}
			s[i][j] = (s[i][j - 1] + f[i][j]) % mod;
		}
	}
	int ans = 0;
	for(int i = 1; i <= n; i++) ans = (ans + s[i][m - 1]) % mod;
	cout << ans;
// 	system("pause");
	return 0;
}
/*
f[i][j] : 前 i 个数,a[i] 在 [j, j + 1)
枚举使 [lst + 1, j] 都在 [rk[j], rk[j + 1]) 的 lst(0 <= lst < i)
f[i][j] = \sum{s[lst][j - 1] * C(rk[j] - rk[j - 1] + cnt - 1, cnt)}
初始化:f[0][0] = 1
C(rk[j + 1] - rk[j], 1) = rk[j + 1] - rk[j]
C(x + 1, y + 1) = jc[x + 1] * jcinv[y + 1] * jcinv[x - y]
C(x, y) = jc[x] * jcinv[y] * jcinv[x - y]
C(x + 1, y + 1) = C(x, y) * (x + 1) * inv[y + 1]
*/2024-07-06 21:45:00 星期六

Interesting Problem Easy/Hard

首先给出一个关键但很显然的性质:一个决策只被前面的决策影响。于是我们可以设计一个区间 \(\rm DP\):设 \(f_{l, r, k}\) 为若 \([1, l - 1]\) 进行了 \(k\) 次操作, 区间 \([l, r]\) 最多可以进行多少操作。转移有两种:

  • 消掉 \([l + 1, r - 1]\) 后再消 \((l, r)\)

  • 找到一个断点 \(k\),先消 \([l, k]\) 再消 \([k + 1, r]\)(注意这里的先后顺序是不固定的,即假如 \([l, k]\) 的可消除对数大于 \([k + 1, r]\) 所需要的消除对数 \(x\),可以通过先操作 \([l, k]\) \(x\) 次后在再进行 \([k + 1, r]\) 的消除操作)。

时间复杂度 \(O(n^4)\),可以通过 \(\rm Easy\)

优化是比较困难的,考虑进一步发掘题目中的性质。观察到答案一定是由一段段全部消除的区间拼接而成的。受到上面的启发,我们可以再次设计出一个 \(\rm DP\)\(f_{l, r}\) 是将区间 \([l, r]\) 全部消除最少所需要 \([1, l]\) 消除多少对。转移是容易的。如何计算答案呢?再设计一个 \(\rm DP\) 即可。

qwq
#include<bits/stdc++.h>
#define int long long 
using namespace std;

const int N = 800 + 10;

int n, f[N][N], g[N], a[N];

int Get(int id, int val){
	int opt = (id - val) / 2;
	if(opt > id / 2 || ((id - val) & 1) || val > id) return -1;
	return opt;
}

void solve(){
	cin >> n; memset(f, 0x3f, sizeof f); memset(g, 0, sizeof g); int INF = f[0][0];
	for(int i = 1; i <= n; i++) cin >> a[i], f[i][i - 1] = 0;
	for(int len = 2; len <= n; len += 2){
		for(int l = 1; l + len - 1 <= n; l++){
			int r = l + len - 1;
			if(Get(l, a[l]) != -1 && Get(l, a[l]) >= f[l + 1][r - 1]) f[l][r] = Get(l, a[l]);
			for(int k = l; k < r; k++){
				f[l][r] = min(f[l][r], max(f[l][k], f[k + 1][r] - (k - l + 1) / 2));
			}
//			if(f[l][r] > (l - 1) / 2 || (r - l + 1) % 2) f[l][r] = INF;
//			cout << l << " " << r << " " << f[l][r] << "\n";
		}
	}
	for(int i = 2; i <= n; i++){
		g[i] = g[i - 1];
		for(int j = 1; j <= i; j++) if(g[j - 1] >= f[j][i]) g[i] = max(g[i], g[j - 1] + (i - j + 1) / 2);
//		cout << g[i] << "\n";
	}
	cout << g[n] << "\n";
}

signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0); cout.tie(0);
	int T; cin >> T; while(T--) solve();
	
	return 0;
}

ABC176F Brave CHAIN

容易有简单 \(O(n^3)\) \(\mathrm {DP}\):设 \(f_{i, a, b}\) 是前 \(3\times i + 2\) 个保留 \((a, b)\) 时的最大答案。有一下三类转移:

  • 不换卡:\(f_{i, a, b} = f_{i - 1, a, b} + [c = d = e]\)

  • 换一张:\(f_{i, a, c} = f_{i - 1, a, b} + [b = d = e]\)

  • 换两张:\(f_{i, c, d} = f_{i - 1, a, b} + [a = b = e]\)

当然有一些类似的不予考虑。我们发现其实没有多少状态被影响到了,若被影响到的多,原因也是 \(c = d = e\),于是猜测影响到的状态不多。接下来考虑什么样的状态 \(f_{i, a, b}\) 可能会被这三类转移分别更新。

对于第一种,显然对于任意 \(f_{i, a, b}\) 有没有被更新取决于 \([c = d = e]\),而这三者又是固定的,于是维护一个全局加标记即可。

对于第二种,被影响到的状态只有 \(c\) 这一列,若 \([b = d = e]\) 成立,此时转移过来的状态是固定的。若不满足,其实就是对于固定的 \(a\),让 \(f_{i, a, c}\) 更新为这一行的最大值,维护一下即可。

对于第三种,若满足 \([a = b = e]\),两边都是确定的。直接转移即可。若不满足,显然等价于更新为全局最大值,也是维护一下即可。

qwq
#include<bits/stdc++.h>
using namespace std;

const int N = 2000 + 10;

int n, f[N][N], arr[N * 3];
void MAX(int& x, int y){if(x < y) x = y;}

struct opt{
	int k0, k1, val;
};
void upd(int a, int b, int w){
	MAX(f[a][b], w); MAX(f[a][0], w); MAX(f[0][0], w);
}

signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0); cout.tie(0);
	cin >> n; memset(f, -0x3f, sizeof f); int allsum = 0;
	for(int i = 1; i <= 3 * n; i++) cin >> arr[i];
	upd(arr[1], arr[2], 0); upd(arr[2], arr[1], 0);
	for(int i = 1; i < n; i++){
		queue<opt> Q; int c = arr[3 * i], d = arr[3 * i + 1], e = arr[3 * i + 2], ad = 0;
		// f[i][a][b]
		if(c == d && d == e) ad++, allsum++;
		// f[i][a][c], f[i][a][d], f[i][a][e]
		for(int a = 1; a <= n; a++){
			if(d == e) Q.push((opt){a, c, f[a][d] + 1});
			Q.push((opt){a, c, f[a][0]});
			if(c == e) Q.push((opt){a, d, f[a][c] + 1});
			Q.push((opt){a, d, f[a][0]});
			if(d == c) Q.push((opt){a, e, f[a][d] + 1});
			Q.push((opt){a, e, f[a][0]});
		}
		// f[i][c][d], f[i][c][e], f[i][d][e]
		Q.push((opt){c, d, f[e][e] + 1}); Q.push((opt){c, d, f[0][0]});
		Q.push((opt){c, e, f[d][d] + 1}); Q.push((opt){c, e, f[0][0]});
		Q.push((opt){d, e, f[c][c] + 1}); Q.push((opt){d, e, f[0][0]});
		// update
		while(!Q.empty()){
			opt ths = Q.front(); Q.pop(); 
			int a = ths.k0, b = ths.k1, w = ths.val - ad;
			upd(a, b, w); upd(b, a, w);
		}
	}
	int ans = 0;
	for(int a = 1; a <= n; a++){
		for(int b = 1; b <= n; b++){
			MAX(ans, f[a][b] + (a == b && b == arr[n * 3]));
		}
	}
	cout << ans + allsum;

	return 0;
}
/*
f[i][a][b] = f[i - 1][a][b] + [c = d = e]
f[i][a][c] = f[i - 1][a][b] + [b = d = e]
f[i][c][d] = f[i - 1][a][b] + [a = b = e]
*/

AGC007E Shik and Travel

非常厉害的一道题!

首先容易发现答案具有单调性,于是先通过二分答案 \(V\) 转化成为判定问题。

接下来注意到一条边只能走两次,其实就是限制了当进入一棵子树,必须先把里面的所有点经过之后才能离开这棵子树。也就是说,当处理到一颗子树时,需要把它的一颗子树处理完,然后从一个叶子到另一颗子树的叶子,再处理完这个子树,然后离开。

不难发现我们只需要知道两颗子树从 \(u\) 到第一个叶子的距离以及最后一个叶子到 \(u\) 的距离。于是容易设计出状态 \(f_{u, a, b}\) 表示是否有一种处理方案满足费用 \(\le V\),且 \(u\) 到第一个叶子的距离为 \(a\),最后一个叶子到 \(u\) 的距离为 \(b\)。状态转移就是

\[f_{u, a, b} = f_{ls, a - da, i - da} \& f_{rs, j - db, b - db} \& [i + j \le V] \]

但是想要再优化就没有那么简单了。首先需要注意到一个显然的但是容易被忽略的性质:若 \(x_1 \le x_2, y_1 \le y_2\)\(f_{x_2, y_2}\)\(f_{x_1, y_1}\) 严格偏序,即没有用。于是我们可以将这些状态删掉,只记录有用的状态中值为 \(1\) 的,以减小时间复杂度。此时对于一个 \(u\),其有用的 \(f\) 值显然是满足 \(a\) 单调递增,\(b\) 单调递减。于是可以双指针合并一下两颗子树中的信息。

但是这样的时间复杂度?我们设节点 \(u\) 中储存的有用的状态数为 \(siz_u\)。一次转移产生的状态数上限是 \(2 \times \min(siz_{ls}, siz_{rs})\),不难发现这其实等价于一个启发式合并,于是时间复杂度为 \(O(n \log n)\)

code:

qwq
#include<bits/stdc++.h>
#define ll long long
#define pir pair<ll, ll>
using namespace std;

const int N = 2e5 + 10;

int n, son[N][3];
ll dis[N][2];
vector<pir> f[N], g[N], vec[N];
ll lim;

void clrvec(vector<pir> &v){vector<pir> __qwq; swap(__qwq, v);}

void Merge_array(int u){
    int j = 0; vector<pir> tmp;
    for(int i = 0; i < f[u].size(); i++){
           while(j < g[u].size() && (g[u][j].first < f[u][i].first || (g[u][j].first == f[u][i].first && g[u][j].second < f[u][i].second))) tmp.push_back(g[u][j++]);
           tmp.push_back(f[u][i]);
    }
    while(j < g[u].size()) tmp.push_back(g[u][j++]);
    if(tmp.empty()) return;
    int ttt = 0; vec[u].push_back(tmp[0]);
    for(int i = 1; i < tmp.size(); i++){
            if(tmp[i].second < vec[u][ttt].second) ttt++, vec[u].push_back(tmp[i]); 
    }
}

void dfs(int u){
    if(!son[u][2]){ vec[u].push_back(make_pair(0ll, 0ll)); return;}
    for(int i = 0; i < 2; i++) dfs(son[u][i]);
    int ls = son[u][0], rs = son[u][1], j = 0, sum = dis[u][0] + dis[u][1];
    for(int i = 0; i < vec[ls].size(); i++){
           while(j < vec[rs].size() && vec[ls][i].second + vec[rs][j].first + sum <= lim) j++;
           if(j) f[u].push_back(make_pair(vec[ls][i].first + dis[u][0], vec[rs][j - 1].second + dis[u][1]));
    }
    swap(ls, rs); j = 0;
    for(int i = 0; i < vec[ls].size(); i++){
           while(j < vec[rs].size() && vec[ls][i].second + vec[rs][j].first + sum <= lim) j++;
           if(j) g[u].push_back(make_pair(vec[ls][i].first + dis[u][1], vec[rs][j - 1].second + dis[u][0]));
    }
    Merge_array(u); clrvec(f[u]); clrvec(g[u]);
    //cout << u << " " << vec[u].size() << " qwq" << "\n";
    //for(int i = 0; i < vec[u].size(); i++) cout << vec[u][i].first << " " << vec[u][i].second << "\n";
}


bool check(ll V){
       //cout << "\n" << V << "\n" << ":::" << "\n";
       for(int i = 1; i <= n; i++) clrvec(f[i]), clrvec(g[i]), clrvec(vec[i]);
       lim = V; dfs(1);
       return vec[1].size();
}

signed main(){
       ios::sync_with_stdio(0);
       cin.tie(0); cout.tie(0);
       cin >> n;
       for(int i = 2; i <= n; i++){
            int x, y; cin >> x >> y;
            son[x][son[x][2]] = i; dis[x][son[x][2]] = y; ++son[x][2];
       }
       ll l = 0, r = 3e10, ans = 3e10;
       while(l <= r){
            ll mid = (l + r >> 1);
            if(check(mid)) r = mid - 1, ans = mid;
            else l = mid + 1;
       }
       cout << ans;

       return 0;
}

P6383 『MdOI R2』Resurrection

首先观察答案形态,不难发现等价于每次找到当前树上两点所在连通块的根进行连边。现在考虑断边 \((u, v)\) 形成的 \(G\) 的形态,可以发现这个东西只跟 \(1 \to v\) 上每条边断边时间的相对关系有关。现在考虑这些边断边的相对时间 \(t_0, t_1, ..., t_m\) 以及点 \(u_1, u_2, ...., u_m\),观察出 \(u_m = u\) 会与 \(u_p(p = \max\{p|t_p < t_m\})\) 连边。那么当 \(t_0, ..., t_{m - 1}\) 确定时,不同的 \(t_m\) 可以造成不同的图 \(G\) 的个数为 \(\sum_{i = 0}^{m - 1}[{t_i > \max_{j = 0}^{i - 1}\{t_j\}]}\)。其实就是 \(t_0, ..., t_m\) 单调递增的单调栈大小,那么当 \(u\) 的单调栈大小为 \(w_u\) 的时候,\(v\) 的单调栈大小就可以是 \([2, w_u + 1]\)

所以不难设计出 \(\mathrm {DP}\) 状态:\(f_{u, i}\) 表示 \(1 \to u\) 的单调栈大小为 \(i\) 时,\(u\) 子树内不同方案数。根据上面的分析,有转移 \(f_{u, i} = \prod_{v \in son(u)}\sum_{j = 2}^{i + 1}{f_{v, j}}\)。前缀和优化即可做到 \(O(n^2)\)

qwq
#include<bits/stdc++.h>
#define ll long long
#define pb emplace_back
using namespace std;

const int N = 3e3 + 10, mod = 998244353;

int n;
ll f[N][N], s[N];
vector<int> G[N];
int Mod(int x){return (x % mod + mod) % mod;}

void dfs(int u, int fa){
	for(int j = 1; j <= n; j++) f[u][j] = 1;
	if(u != n) f[u][1] = 0;
	for(auto v : G[u]){
		if(v == fa) continue; dfs(v, u); 
		for(int j = 1; j <= n; j++) s[j] = (s[j - 1] + f[v][j]) % mod; 
		for(int j = 1; j <= n; j++) f[u][j] = f[u][j] * Mod(s[j + 1] - s[1]) % mod;
	}
//	for(int i = 1; i <= n; i++) cout << u << " " << i << " " << f[u][i] << "\n";
}

signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0); cout.tie(0);
	cin >> n;
	for(int i = 1; i < n; i++){
		int x, y; cin >> x >> y;
		G[x].pb(y); G[y].pb(x);
	}
	dfs(n, 0); cout << f[n][1] << "\n";

	return 0;
}

P6846 [CEOI2019] Amusement Park

首先不难注意到假如一个有向图用 \(c\) 次操作定向成了一个 \(\rm DAG\),那么一定可以用 \(m - c\) 次操作再把它定向成反的 \(\rm DAG\),所以问题转化为求定向后生成 \(\rm DAG\) 数量,最后再乘上 \(\frac{m}{2}\)

考虑 \(\rm DP\),并进行 子结构划分即考虑怎么把一个 \(\rm DAG\) 扣掉一些点之后还变成一个 \(\rm DAG\)。不难发现可以枚举 \(0\) 度点,但是这样还可能会算重。再考虑什么时候会算重,不难发现一个 \(0\) 度点集合 \(S\),所有的子集都会被多算一遍,那么直接子集容斥即可。

时间复杂度 \(O(3^n)\)

qwq
#include<bits/stdc++.h>
#define ll long long
using namespace std;

const int N = 20 + 1, M = N * N + 10, maxS = (1ll << N) + 10;
const ll mod = 998244353;

void ADD(ll &x, ll y){
    x += y; (x >= mod) ? (x -= mod) : x;
}

int n, m, con[N], pd[maxS], pc[maxS];
ll f[maxS];

signed main(){
    ios::sync_with_stdio(0);
    cin.tie(0); cout.tie(0);
    cin >> n >> m;
    for(int i = 1; i <= m; i++){
        int x, y; cin >> x >> y; x--; y--;
        con[x] |= (1ll << y); con[y] |= (1ll << x);
    } f[0] = 1;
    for(int S = 1; S < (1ll << n); S++){
        int st = 0; pc[S] = pc[S >> 1] + (S & 1);
        for(int i = 0; i < n; i++) if(S & (1ll << i)) st |= con[i];
        pd[S] = (!(S & st)); //cout << S << " " << st << " " << pd[S] << "\n";
        for(int S0 = S; S0; S0 = S & (S0 - 1)){
            if(!pd[S0]) continue; 
            //cout << S << " " << S0 << " " << f[S ^ S0] << "\n";
            ADD(f[S], f[S ^ S0] * ((pc[S0] & 1ll) ? 1ll : (mod - 1ll)) % mod);
        }
        //cout << S << " " << f[S] << "\n";
    } cout << f[(1ll << n) - 1] * m % mod * ((mod + 1) / 2) % mod;

    return 0;
}

P10197 [USACO24FEB] Minimum Sum of Maximums P

首先记一个转化:\(\max(a, b) = \frac{1}{2}(a + b + |a - b|)\)

在两边插入两个固定的极大值,然后前面的 \(a + b\) 都是确定的了,最后把极大值的贡献剔除即可。问题转化为求 \(\min{\sum{|a_i - a_{i + 1}|}}\)

考虑每段内如何排列最好,显然是升序排序最好(降序的话就把 \(L, R\) 交换就一样了),那么每段的贡献就是 \(|L - ma| + |R - mi| + ma - mi\)。不难发现 \(ma\) 的贡献随着其本身的增大而增大,\(mi\) 则反之。因此我们想让 \(ma\) 变小,\(mi\) 变大。记每一段的"区间"为 \([mi_i, ma_i]\),于是可以得到结论,任意两个不同区间,要么相离要么包含。反证法即可。

因此我们可以设计一个区间 \(\rm DP\)\(f_{l, r, s}\) 表示已经填了 \([l, r]\) 区间的数(排序后的),\(s\) 集合中的段已经被填满了。转移如下:

  • 合并相离的段:\(f_{l, r, s} = f_{l, l + siz[s0] - 1, s0} + f_{l + siz[s0], r, s - s0}, s0 \in s\)

  • 确定一个段 \(i\) 的最大最小值:

    • 确定的段长度只有 \(1\)(这个情况只有在只剩一个数的时候使用,即 \(l = r\),最后合并即可,否则转移正确性无法保证):\(f_{l, r, s} = |R_i - a_l| + |a_l - L_i|\)

    • 一般情况:\(f_{l, r, s} = f_{l + 1, r - 1, s} + |R_i - a_l| + a_r - a_l + |L_i - a_r|\)

时间复杂度 \(O(3^nn^2)\)

qwq
#include<bits/stdc++.h>
#define ll long long
using namespace std;

const int N = 300 + 5, MAXV = (1ll << 7) + 5, INF = 1e6 + 5;

int n, m, a[N], id[N], L[N], R[N], len[N], siz[MAXV], cnt, vis[N];
ll ans = 2 * INF, f[N][N][MAXV];

void chkmin(ll& x, ll y){if(y < x) x = y;}

signed main(){
    ios::sync_with_stdio(0);
    cin.tie(0); cout.tie(0);
    memset(f, 0x3f, sizeof f);
    cin >> n >> m; n += 2; a[1] = a[n] = INF; m += 2; id[1] = 1; id[m] = n;
    for(int i = 2; i < n; i++) cin >> a[i], ans += 2 * a[i];
    for(int i = 2; i < m; i++) cin >> id[i], id[i]++, vis[id[i]] = 1;
    for(int i = 1; i < m; i++){
        if(id[i + 1] == id[i] + 1) ans += abs(a[id[i]] - a[id[i] + 1]);
        else{
            len[cnt] = id[i + 1] - id[i] - 1;
            L[cnt] = a[id[i]], R[cnt] = a[id[i + 1]];
            if(L[cnt] > R[cnt]) swap(L[cnt], R[cnt]);
            cnt++;
        }
    }
    int tt = 0;
    for(int i = 2; i < n; i++) if(!vis[i]) a[++tt] = a[i];
    sort(a + 1, a + tt + 1); n = tt;
    for(int i = 1; i <= n; i++) f[i + 1][i][0] = 0;
    for(int s = 0; s < (1ll << cnt); s++)
        for(int i = 0; i < cnt; i++){
            if(s & (1ll << i)) siz[s] += len[i];
        }
    for(int lll = 1; lll <= n; lll++){
        for(int l = 1; l + lll - 1 <= n; l++){
            int r = l + lll - 1;
            for(int s = 0; s < (1ll << cnt); s++){
                //if(s == 0) f[l][r][s] = 0;
                f[l][r][s] = min(f[l + 1][r][s], f[l][r - 1][s]);
                if(siz[s] > r - l + 1) continue;
                for(int s0 = s; s0; s0 = s & (s0 - 1)) chkmin(f[l][r][s], f[l][l + siz[s0] - 1][s0] + f[l + siz[s0]][r][s ^ s0]);
                for(int i = 0; i < cnt; i++){
                    if((1ll << i) & s){
                        ll val = abs(R[i] - a[r]) + a[r] - a[l] + abs(a[l] - L[i]);
                        if(siz[s] == 1 && len[i] == 1) chkmin(f[l][r][s], val);
                        else if(len[i] > 1) chkmin(f[l][r][s], f[l + 1][r - 1][s ^ (1ll << i)] + val);
                    }
                }
                //cout << l << " " << r << " " << s << " " << f[l][r][s] << "\n";
            }
        }
    }
    cout << (ans + f[1][n][(1ll << cnt) - 1]) / 2 - 2 * INF;

    return 0;
}

AGC061E Increment or XOR

很好的一道划分 \(\rm DP\) 阶段的题。

首先观察结构,发现 \(+1\) 等价于把后面的一段 \(1\) 变为 \(0\),然后假如继续 \(+1\),那么相当于一个一样的阶段(从 \(0 \to 2^k - 1 \to 0\) ),即 子问题。且当 \(+1\) 没有达到 \(i\) 位时,\(i\) 以上的位都不会被影响到。

于是我们考虑把前 \(i\) 位的操作划分为一个个阶段,不难发现这一个一个阶段的起点都可以被规约为 \(0/S\),终点同理可以规约到 \(0/T\)。于是考虑设状态 \(f_{i, x, y, s}\) 为考虑前 \(i\) 位(\(0, 1 .., i\) 位),起点是 \(S/0\),终点是 \(T/2^{i + 1}\),选择奇数次异或操作的集合是 \(s\),需要最小的操作数是多少。接下来考虑 \(i \to i + 1\) 的转移。

  • 已经满足条件,不需要操作:此时需要满足 \(state_s \oplus Start_x = End_y\),直接 \(f_{i, x, y, s} \to f_{i + 1, x, y, s}\)

  • 由若干个阶段拼起来:首先从 \(x\) 进行操作到 \(2^{i + 2}\),接着通过若干次进位,最后一个阶段从 \(2^{i + 2}\)\(y\)。注意因为我们的进位至多进行一次,因此每次中间的进位操作一定要满足 \(state_s\)\(i + 1\) 位是 \(1\)。具体转移是 \(f_{i, x, 1, s_1} + f_{i, 1, 1, s_2} + ... + f_{i, 1, 1, s_{ss - 1}} + f_{i, 1, y, s_{ss}} \to f_{i + 1, x, y, \oplus s}\)

这个东西就是一个最短路的形式,\(\rm dijkstra\) 优化即可。

qwq
#include<bits/stdc++.h>
#define ll long long
#define int long long
using namespace std;

const int MAXV = (1ll << 8) + 10, N = 10;
const ll INF = 1e18;

void chkmin(ll& x, ll y){if(y < x) x = y;}
bool ex(int s, int i){return (s & (1ll << i));}

int n, a[N], S, T, st[MAXV], qwq[4];
bool vis[MAXV];
ll c[N], f[2][2][MAXV], g[2][2][MAXV], A, w[MAXV], dis[MAXV];

void upd(int bt, int x){
    //cerr << bt << "\n";
    for(int s = 0; s < (1 << n); s++){
      vis[s] = 0;
      if(ex(st[s], bt) == qwq[x]) dis[s] = g[x][1][s];
      else dis[s] = INF;
    }
    for(int rnd = 1; rnd <= (1 << n); rnd++){
        int p = -1;
        for(int s = 0; s < (1 << n); s++) if((p == -1 || dis[p] > dis[s]) && (!vis[s])) p = s;
        if(p == -1 || dis[p] >= INF) return;
        vis[p] = 1; //cerr << p << " " << dis[p] << "\n";
        for(int s = 0; s < (1 << n); s++){
            if(vis[s] || (!ex(st[p ^ s], bt))) continue;
            chkmin(dis[s], dis[p] + g[1][1][s ^ p]);
        }
        for(int s = 0; s < (1ll << n); s++){
           for(int y = 0; y < 2; y++) if(qwq[2 + y] == (ex(st[s], bt) ^ 1)) chkmin(f[x][y][s ^ p], dis[p] + g[1][y][s]);  
        }
    }
}

signed main(){
    ios::sync_with_stdio(0);
    cin.tie(0); cout.tie(0);
    cin >> n >> S >> T >> A; qwq[1] = 0; qwq[3] = 1;
    for(int i = 0; i < n; i++) cin >> a[i] >> c[i];
    for(int s = 0; s < (1 << n); s++)
        for(int i = 0; i < n; i++)
            if(ex(s, i)) w[s] += c[i], st[s] ^= a[i];
    for(int x = 0; x < 2; x++)
        for(int y = 0; y < 2; y++)
            for(int s = 0; s < (1 << n); s++) f[x][y][s] = w[s] + y * A;
    for(int i = 0; i < 40; i++){
        qwq[0] = ex(S, i); qwq[2] = ex(T, i);
        for(int x = 0; x < 2; x++)
            for(int y = 0; y < 2; y++)
                for(int s = 0; s < (1 << n); s++){
                    //cerr << x << " " << y << " " << s << " " << f[x][y][s] << "\n";
                    g[x][y][s] = f[x][y][s];
                    if((qwq[x] ^ ex(st[s], i)) != qwq[2 + y]) f[x][y][s] = INF;
                }
        for(int x = 0; x < 2; x++) upd(i, x);
    }
    ll ans = INF;
    for(int s = 0; s < (1 << n); s++) chkmin(ans, f[0][0][s]);
    if(ans >= INF){cout << -1; return 0;}
    cout << ans;

    return 0;
}

AGC052C Nondivisible Prefix Sums

首先由于可以直接重排,顺序是不重要的,考虑一个一个将这些数加入序列中。假设现在已经确定了一个前缀信息,考虑接下来放什么数,显然只会有一种数是不合法的,为了保持合法,那么一个很 naive 的想法就是保持手上有至少 \(2\) 种数。于是当序列中没有绝对众数时就是合法的。但有绝对众数就不合法吗?显然不是。

接下来,为了方便分析,不妨让所有的数都乘上众数的逆元,这样众数就是 \(1\) 了。这一步非常神仙啊。设其他的数构成序列 \(b\),显然我们尽量放 \(1\) 更好,所以我们让 \(a_1, a_2 ..., a_{p-1}\) 都是 \(1\),那么 \(a_p\) 就不能是 \(1\) 了,就随便放一个 \(b_i\) 即可。那么 \(a_{p + 1}, ..., a_{p + p - b_i - 1}\) 都可以放 \(1\) 了。于是最多放 \(1\) 的个数就是 \(\sum_{i}{p - b_i} + p - 1\),所以这个是必要条件。再考虑是否充分,唯一的问题就是假设 \(1\) 用完后剩下有 \(b_i\) 出现绝对众数的怎么办。那在 \(1\) 不再是剩下的数的众数时,不难发现此时已经没有绝对众数了,因此合法了。

于是一个序列合法的充要条件就是:\(n - cnt \le \sum_{i = 1}^{cnt} {p-b_i}\)

直接算合法的其实不好做,于是考虑算不合法的。那么后面的东西不能超过 \(n\),直接 \(\rm DP\) 加前缀和优化即可。

最后只剩总方案数了,但是注意上面的东西都没有考虑到一个东西,就是总共和等于 \(p\) 的倍数的情况。这个在写一个递推即可。

qwq
#include<bits/stdc++.h>
#define ll long long
using namespace std;

const int N = 5000 + 10;
const ll mod = 998244353;

int n;
ll P, jc[N], jcinv[N], f[N], g[N], h[N], sum[N];
void ADD(ll& x, ll y){x += y; (x >= mod) ? x -= mod : 0;}

ll qpow(ll x, int y){
    ll ret = 1;
    for(; y; y >>= 1, x = x * x % mod) if(y & 1) ret = ret * x % mod;
    return ret;
}

ll C(int x, int y){return jc[x] * jcinv[x - y] % mod * jcinv[y] % mod;}

signed main(){
    ios::sync_with_stdio(0);
    cin.tie(0); cout.tie(0);
    cin >> n >> P; jc[0] = jcinv[0] = 1;
    for(int i = 1; i <= n; i++) jc[i] = jc[i - 1] * i % mod, jcinv[i] = qpow(jc[i], mod - 2);
    g[0] = 1;
    for(int i = 1; i <= n; i++) g[i] = (P - 1) * h[i - 1] % mod, h[i] = (g[i - 1] + h[i - 1] * (P - 2) % mod) % mod;
    ll ans = (P - 1) * h[n] % mod; sum[0] = 1;
    if(n % P && P <= n) ADD(ans, mod - (P - 1));
    for(int i = 1; i <= n; i++){
        for(int j = 1; j <= n; j++) sum[j] = (f[j] + sum[j - 1]) % mod;
        for(int j = 1; j <= n; j++){
            f[j] = (sum[j - 1] + mod - ((j - (P - 1) >= 0) ? sum[j - P + 1] : 0ll)) % mod;
            if((n - i - j) % P != 0 && n - i > j + P - 1) ADD(ans, mod - f[j] * C(n, i) % mod * (P - 1) % mod);
        } sum[0] = 0;
    } cout << ans;

    return 0;
}

AGC040E Prefix Suffix Addition

非常好 \(\rm DP\) 题。

\(a\) 序列的产生可以看做前后缀分别操作最后累加导致的,分开考虑,先考虑只有前缀操作有效的情况可能生成什么样的序列。不难发现,这可以由一些升序序列拼接组成。同样的,只有后缀操作产生的序列只有可能是由一些降序序列拼成。

尝试使用 \(\rm DP\),设 \(f_{i, j}\) 为考虑前 \(i\) 个数,前缀在第 \(i\) 个数上加了 \(b_i = j\) 次,转移如下:

\[f_{i, j} = \min^{a_i}_{k = 0}{f_{i - 1, k} + [j > k] + [a_i - j < a_{i - 1} + k]} \]

不难发现 \(f_i\) 是单调不降的,且 \(f_i\) 有很多值相同的连续段,具体如何呢?打表可以发现,满足 \(\forall x, f_{i, x} - f_{i, 0} \le 2\) 。这是因为假设 \(f_{i, 0}\) 的最优决策是 \(k\),那么 \(f_{i, x} \le f_{i - 1, k} + 2\),于是进一步得 \(f_{i, x} \le f_{i, 0} + 2\)

然后直接维护连续段即可,因为我不想分讨所以最后写的是二分做法。

qwq
#include<bits/stdc++.h>
#define ll long long
#define pb emplace_back
using namespace std;

const int N = 2e5 + 10;

int n, a[N];
struct node{
   int l, r, val;
};
vector<node> f[N];

void chkmin(int& x, int y){if(y < x) x = y;}

int get(int i, int pos){
   int ans = 1e9;
   for(auto qwq : f[i - 1]) chkmin(ans, qwq.val + (pos < qwq.l) + (a[i - 1] - qwq.l < a[i] - pos));
   return ans;
}

signed main(){
   ios::sync_with_stdio(0);
   cin.tie(0); cout.tie(0);
   cin >> n;
   for(int i = 1; i <= n; i++) cin >> a[i];
   f[0].pb((node){0, 0, 0});
   for(int i = 1; i <= n + 1; i++){
      for(int l = 0; l <= a[i];){
         int r = l, val = get(i, l);
         int L = l, R = a[i], mid;
         while(L <= R){
            mid = (L + R >> 1);
            if(get(i, mid) == val) L = mid + 1, r = mid;
            else R = mid - 1;
         } f[i].pb((node){l, r, val}); l = r + 1;
      }
   } cout << f[n + 1][0].val;

   return 0;
}

P8386 PA 2021 Od deski do deski

考虑如何刻画一个合法的序列,即考虑如何判定。设 \(f_i\) 为前 \(i\) 个数是否满足条件,则 \(f_i = \max _{j = 1} ^ {i - 1}\{[a_i = a_j]f_{j - 1}\}\)。这样直接做是 \(O(n^2)\) 的。直接维护集合 \(S = \{a_i | [f_{i - 1} = 1]\}\) 即可 \(O(n) / O(n \log n)\)

然后考虑计数,注意到我们不需要 \(S\) 的所有信息,具体的,只需知道 \(|S|\),因为我们并不关心具体是什么颜色。于是设 \(f_{i, j}\)\(i\) 个数,有 \(|S| = j\) 的合法方案数。然后设 \(g_{i, j}\) 为不合法的方案数。转移容易 \(O(1)\)

qwq
#include<bits/stdc++.h>
#define ll long long
#define pb emplace_back
#define pir pair<int, ll>
#define fi first
#define se second
#define inv(x) qpow(x, mod - 2)
#define il inline
#define mkpir make_pair
using namespace std;

const int N = 5000 + 10, M = 2e5 + 10;
const ll mod = 1e9 + 7;

il ll qpow(ll& x, ll y){
  ll ret = 1;
  for(; y; y >>= 1, x = x * x % mod) if(y & 1) ret = ret * x % mod;
  return ret;
}
il void chkmin(long long& x, long long y){if(y < x) x = y;}
il void chkmin(int& x, int y){if(y < x) x = y;}
il void chkmax(int& x, int y){if(y > x) x = y;}
il void chkmax(long long& x, long long y){if(y > x) x = y;}
il void ADD(ll& x, ll y){x += y; (x >= mod) ? (x -= mod) : 0;}
il void MUL(ll& x, ll y){x = x * y % mod;}

#define int long long
int n, m;
ll f[N][N], g[N][N];

signed main(){
  ios::sync_with_stdio(0);
  cin.tie(0); cout.tie(0);
  cin >> n >> m; f[0][0] = 1;
  for(int i = 0; i < n; i++){
   for(int j = 0; j <= n; j++){
      ADD(f[i + 1][j], (f[i][j] + g[i][j]) * j % mod);
      ADD(g[i + 1][j], g[i][j] * (m - j) % mod); 
      ADD(g[i + 1][j + 1], f[i][j] * (m - j) % mod);
    }
  } ll ans = 0;
  for(int i = 1; i <= min(n, m); i++) ADD(ans, f[n][i]);
  cout << ans;

  return 0;
}

AGC022E Median Replace

还是首先考虑如何判定,然后再考虑 \(\rm DP\)。考虑把 \(0, 1\) 逐个加入一个栈中,然后把能够提前删除的删去,然后再观察结果的序列:

加入的是 \(0\)

  • 栈顶有 \(2\)\(0\):则可以提前消除这 \(3\)\(0\),显然不劣。

加入的是 \(1\)

  • 栈顶有 \(0\):直接消去,这是因为 \(0\) 最后还是要消去,不如只用一个 \(1\) 消去。

那么容易发现结果序列的形态一定是若干个 \(1\) 后面再跟 \(\le 2\)\(0\)。合法的条件就是 \(1\) 的个数大于等于 \(0\) 的个数。然后显然可以对这个东西做 \(\rm DP\),但是这样是 \(O(n^2)\) 的,因为 \(1\) 的个数可能很多。但是我们只关注 \(1\) 是否 \(\ge 2\) 即可,于是 \(\ge 2\)\(1\) 个数都可以把它变成 \(2\) 个,(因为 \(0\) 的个数不需要前面的都可以保持 \(\le 2\))。那么这样就是 \(O(3^2 n)\) 的了。

qwq
#include<bits/stdc++.h>
#define ll long long
#define pb emplace_back
#define pir pair<int, ll>
#define fi first
#define se second
#define inv(x) qpow(x, mod - 2)
#define il inline
#define mkpir make_pair
using namespace std;

const int N = 3e5 + 10, M = 2e5 + 10;
const ll mod = 1e9 + 7;

il ll qpow(ll& x, ll y){
  ll ret = 1;
  for(; y; y >>= 1, x = x * x % mod) if(y & 1) ret = ret * x % mod;
  return ret;
}
il void chkmin(ll& x, ll y){if(y < x) x = y;}
il void chkmin(int& x, int y){if(y < x) x = y;}
il void chkmax(int& x, int y){if(y > x) x = y;}
il void chkmax(ll& x, ll y){if(y > x) x = y;}
il void ADD(ll& x, ll y){x += y; (x >= mod) ? (x -= mod) : 0;}
il void MUL(ll& x, ll y){x = x * y % mod;}

int n;
ll f[3][3], g[3][3];
string str;

void upd0(){
  for(int i = 0; i < 3; i++){
    for(int j = 0; j < 3; j++){
      if(j == 2) ADD(f[i][1], g[i][j]);
      else ADD(f[i][j + 1], g[i][j]);
    }
  }
}

void upd1(){
  for(int i = 0; i < 3; i++){
    for(int j = 0; j < 3; j++){
      if(j > 0) ADD(f[i][j - 1], g[i][j]);
      else ADD(f[min(i + 1, 2)][j], g[i][j]);
    }
  }
}

signed main(){
  ios::sync_with_stdio(0);
  cin.tie(0); cout.tie(0);
  cin >> str; n = str.size(); str = "*" + str;
  f[0][0] = 1;
  for(int i = 1; i <= n; i++){
    for(int c0 = 0; c0 < 3; c0++)
      for(int c1 = 0; c1 < 3; c1++) g[c0][c1] = f[c0][c1], f[c0][c1] = 0;
    if(str[i] == '?') upd0(), upd1();
    else if(str[i] == '1') upd1();
    else upd0();
  }
  ll ans = 0;
  for(int i = 0; i < 3; i++) for(int j = 0; j <= i; j++) ADD(ans, f[i][j]);
  cout << ans;

  return 0;
}

集训队互测 2022 Range Minimum Element

首先考虑如何判定一个 \(b\) 序列是否可能存在一个 \(a\) 构造出。考虑贪心地构造:

  • 初始令 \(a\) 序列中的所有元素为 \(+\inf\)。考虑按值从大到小逐个加入区间,假设现在只考虑了 \(b_x > i\) 的区间,准备加入 \(b_x = i\) 的区间。依次考虑这些区间 \([l_x, r_x]\),把 \([l_x, r_x]\) 中的所有 \(+\inf\) 的位置改成 \(i\)。若 \([l_x, r_x]\) 中不存在 \(+\inf\)\(i\) 这两种值,那么 \(b\) 显然不可能完成。若最后 \(a\) 序列中还有 \(+\inf\),赋值成 \(1\) 即可。

不难发现,由于题目中本来就有映射 \(f: \{a\} \to \{b\}\),现在构造出了一个映射 \(g: S(b) \to \{a\}\)\(S(b)\) 是合法的 \(b\) 序列集合),于是这样构造出来的序列与合法的 \(b\) 构成双射

这其实给了一个启发,若题目要求对某个映射后的结果序列进行计数,可以考虑再构造一个结果到原结构的单射,这样就自然构成了双射。

那么接下来考虑对满足这个过程的 \(a\) 的特殊结构进行 \(\rm DP\)。那么首先找出序列中的第一个 \(1\) 的位置 \(x\) 并分割成 \([1, x - 1]\)\([x + 1, n]\) 两个区间。那么唯一的限制就只是要求 \([1, x - 1]\) 全部被覆盖,并且这个区间内的数值域 \(\ge 2\)。那么这个东西启发我们用区间 \(\rm DP\) 来刻画。但是值域 \(\ge 2\) 的条件很难与 \(\ge c\) 的条件一起考虑。但是你注意到把 \(\max\) 换成 \(\min\) 也是一致的。于是设 \(f_{l, r, lim}\) 是区间 \([l, r]\),只考虑 \(a_x \le lim\) 的值时,不同序列的数量。转移是类似的:(令 \(I_{l, r}\) 为区间 \([l, r]\) 中的区间是否可以完全覆盖区间 \([l, r]\),不难 \(O(nm)\) 求出)

  • \(f_{l, r, lim} \gets f_{l, r, lim - 1} \times I_{l, r}\)

  • \(f_{l, r, lim} \gets f_{l, x, lim - 1} \times I_{l, x} \times f_{x + 1, r, lim}\)

那么这样就是 \(O(n^3c)\) 的了。然后注意到这个 \(\rm DP\) 形式都是简单加乘,所以 \(f_{l, r, lim}\) 其实是关于 \(lim\)\(r - l\) 次多项式,拉插优化即可做到 \(O(n^4)\)

qwq
#include<bits/stdc++.h>
#define ll long long
#define pb emplace_back
#define pir pair<int, ll>
#define fi first
#define se second
#define inv(x) qpow(x, mod - 2)
#define il inline
#define mkpir make_pair
using namespace std;

const int N = 100 + 10, M = 2e5 + 10;
const ll mod = 998244353;

il ll qpow(ll x, ll y){
  ll ret = 1;
  for(; y; y >>= 1, x = x * x % mod) if(y & 1) ret = ret * x % mod;
  return ret;
}
il void chkmin(ll& x, ll y){if(y < x) x = y;}
il void chkmin(int& x, int y){if(y < x) x = y;}
il void chkmax(int& x, int y){if(y > x) x = y;}
il void chkmax(ll& x, ll y){if(y > x) x = y;}
il void ADD(ll& x, ll y){x += y; (x >= mod) ? (x -= mod) : 0;}
il void MUL(ll& x, ll y){x = x * y % mod;}

int n, m, c, I[N][N], buc[N], cnt;
ll f[N][N][N];
vector<int> Q[N];

signed main(){
  ios::sync_with_stdio(0);
  cin.tie(0); cout.tie(0);
  cin >> n >> m >> c;
  for(int i = 1; i <= m; i++){
    int l, r; cin >> l >> r; Q[r].pb(l);
  }
  for(int l = 1; l <= n; l++){
    for(int i = 1; i <= n; i++) buc[i] = 0;
    cnt = 0;
    for(int r = l; r <= n; r++){
      buc[r]++; cnt++; int qwq = r + 1;
      for(auto v : Q[r]) if(v >= l) chkmin(qwq, v);
      //cerr << qwq << "\n";
      for(int i = r; i >= qwq; i--) if(buc[i]) buc[i] = 0, cnt--;
      I[l][r] = (cnt == 0); //cerr << l << " " << r << " " << I[l][r] << "\n";
    }
  } int V = n + 2;
  for(int len = 1; len <= n; len++){
    for(int l = 1; l + len - 1 <= n; l++){
      int r = l + len - 1;
      for(int lim = 1; lim <= V; lim++){
        if(l == r) f[l][r][lim] = 1;
        else{
          ADD(f[l][r][lim], f[l + 1][r][lim]);
          ADD(f[l][r][lim], f[l][r - 1][lim - 1] * I[l][r - 1]);
        }
        if(I[l][r]) ADD(f[l][r][lim], f[l][r][lim - 1]);
        for(int i = l + 1; i < r; i++) ADD(f[l][r][lim], I[l][i - 1] * f[l][i - 1][lim - 1] * f[i + 1][r][lim] % mod);
        //cerr << l << " " << r << " " << lim << " " << f[l][r][lim] << "\n";
      }
    }
  } //cout << f[1][n][c];
  ll ans = 0;
  for(int i = 1; i <= V; i++){
    ll res = f[1][n][i];
    for(int j = 1; j <= V; j++) if(i != j) MUL(res, (c - j + mod) % mod), MUL(res, inv((i - j + mod) % mod));
    ADD(ans, res);
  } cout << ans;

  return 0;
}
posted @ 2024-10-06 09:51  Little_corn  阅读(28)  评论(0)    收藏  举报