title: DP状态设计方法
tags: []
categories: [OI, 算法专题]

本节的DP,主要用于讲解DP状态的设计,题目大多是状压DP和高维DP。包含一个数位DP。
状压DP的设计主要是以下两种:状压权值(从小到大),状压位置(从左到右)。需要根据题目性质具体问题具体设计选择。

P10561 ICPC 2024 Xi'an I Smart Quality Inspector

原题

考点

状压DP

题意

  • 有 \(N\) 个位置,其中 \(K\) 个位置的值为 \(1, 2, \ldots, K\),其余 \(N-K\) 个位置的值为 \(0\)。
  • 给定 \(M\) 个区间 \([l_i, r_i]\),求 \(\sum_{i=1}^M \max_{j=l_i}^{r_i} A_j\)。
  • 可以重新排列序列 \(A\),求上述和的最小值。

数据范围:\(1 \le K \le N \le 20\),\(1 \le M \le 10^5\)。

思路

看数据范围容易想到使用状压DP。
在大多数题目中,max是不好处理的。因为这个东西对答案造成的贡献不好统计。在本题中,我们考虑如何使一个区间的贡献确定。
状压DP大体上分为两种:状压位置(从左到右),状压权值(从小到大)。本题为了能达到要求,要从大到小状压。这样,单次区间的贡献能够确定。区间数量是未被选中的包含当前位置的区间数量。这个可以用二维前缀和解决。
我当时想这个二维前缀和是不是还要容斥一下啊,实际上不用。看代码易知。

代码

#include <bits/stdc++.h>
using namespace std;
//#define int long long
#define pii pair<int,int>
#define f(i,a,b) for(int i=(a);i<=(b);i++)
#define Dl(a) cout << #a << " : " << a << "\n";
#define D(a) cout << #a << " : " << a;
#define Da(a,i,j) cout << #a << " : ";f(idx,i,j){cout << a[idx] <<" ";}
const int N = 21;
int n,k,m;
int mp[N][N];
int p[N][N];
int dp[(1 << N)];
signed main(){
	//freopen(".in","r",stdin);
	//freopen(".out","w",stdout);
	cin >> n >> k >> m;
	f(i,1,m){
		int l,r; cin >> l >> r;
		mp[l][r]++;
	}
	f(i,1,n){
		f(j,1,n){
			p[i][j] = p[i-1][j] + p[i][j-1] - p[i-1][j-1] + mp[i][j];
//			cout << p[i][j] <<" ";
		}
//		cout << "\n";
	}
	memset(dp,0x3f,sizeof dp);
	dp[0] = 0;
	f(st,0,(1 << n) - 1){
		int i = __builtin_popcount(st) + 1;
		if (i > k) continue;
		f(j,1,n){
			if (st >> (j - 1) & 1){
				continue;
			}
			int l=j,r=j;
			for (l = j;l>=1;l--){
				if (st >> (l - 1) & 1) break;
			}
			for (r = j;r<=n;r++){
				if (st >> (r-1) & 1) break;
			}
			l++;r--;
//			if (st == 2 && (st | (1 << (j-1))) == 3){
//				D(l);D(r);D(j);D((st | (1 << (j-1))));
//				Dl(p[j][r] - p[j][j-1] - p[l-1][r] + p[l-1][j-1]);
//			}
			dp[st | (1 << (j-1))] = min(dp[st | (1 << (j-1))],dp[st] + (p[j][r] - p[j][j-1] - p[l-1][r] + p[l-1][j-1]) * (k - i + 1));
		}
	}
//	Dl(dp[1]);
//	Dl(dp[2]);
//	Dl(dp[4]);
//	Dl(dp[8]);
//	Dl(dp[3]);
	cout << dp[(1 << n) - 1];
	return 0;
}

P4067 SDOI2016 储能表

原题

考点

数位DP

题意

  • 有一个 \(n\) 行 \(m\) 列的表格(行列从 \(0\) 编号),格子 \((i, j)\) 初始能量为 \(i \oplus j\)。
  • 经过 \(k\) 个时间单位后,每个格子的能量减少 \(k\) 但不低于 \(0\),即剩余能量为 \(\max((i \oplus j) - k, 0)\)。
  • 求 \(k\) 个时间单位后整个表格的总能量,对 \(p\) 取模。
  • 多组数据。

数据范围:\(1 \le T \le 5000\),\(1 \le n, m, k \le 10^{18}\),\(1 \le p \le 10^9\)。

思路

数位DP。
由于是异或,按位选择。设计状态为当前位,是否被n限制,是否被m限制,是否被k限制。
把答案拆开,发现需要知道大于k的位置,计为cnt。ans = 异或结果大于k的和 - cnt * k。
到这儿思路就顺了,但是代码我调了很久。
-1 使用1ll左移而不是1
-2 括号加的不对/没加
-3 异或和忘统计了
-4 统计答案在dfs里,而不是外面。实际上,数位DP大多是在外面统计的。

代码

#include <bits/stdc++.h>
using namespace std;
#define int long long
#define pii pair<int,int>
#define f(i,a,b) for(int i=(a);i<=(b);i++)
#define Dl(a) cout << #a << " : " << a << "\n";
#define D(a) cout << #a << " : " << a;
#define Da(a,i,j) cout << #a << " : ";f(idx,i,j){cout << a[idx] <<" ";}
const int N = 71;
int t;
int n, m, k, p;
int bitN[N], bitM[N], bitK[N], cntN, cntM, cntK;
pii vis[N][2][2][2];
int ans;
void init(int n, int m, int k) {
	memset(bitN, 0, sizeof bitN);
	memset(bitM, 0, sizeof bitM);
	memset(bitK, 0, sizeof bitK);
	memset(vis, 0, sizeof vis);
	cntN = cntM = cntK = ans = 0;
	while (n) {
		bitN[cntN++] = n % 2;
		n /= 2;
	}
	while (m) {
		bitM[cntM++] = m % 2;
		m /= 2;
	}
	while (k) {
		bitK[cntK++] = k % 2;
		k /= 2;
	}
	cntN --;cntM--;cntK--;
}
pii bitFill(int i, int bn, int bm, bool limitN, bool limitM, bool limitK);
pii dfs(int i, bool limitN, bool limitM, bool limitK) {
//	Dl(i);
	if (i == -1) {
		return {1,0};
	}
	if (vis[i][limitN][limitM][limitK].first) {
//		ans = (ans + vis[i][limitN][limitM][limitK].second) % p;
		return vis[i][limitN][limitM][limitK];
	}
	int res = 0,res2 = 0;
	pii tmp = bitFill(i, 0, 0, limitN, limitM, limitK);
	res = (tmp.first + res) % p;res2 = (tmp.second + res2) % p;
	tmp = bitFill(i, 0, 1, limitN, limitM, limitK);
	res = (tmp.first + res) % p;res2 = (tmp.second + res2) % p;
	tmp = bitFill(i, 1, 0, limitN, limitM, limitK);
	res = (tmp.first + res) % p;res2 = (tmp.second + res2) % p;
	tmp = bitFill(i, 1, 1, limitN, limitM, limitK);
	res = (tmp.first + res) % p;res2 = (tmp.second + res2) % p;
//	if (i > 0){
//		Dl(res - tmp);
//	}
	vis[i][limitN][limitM][limitK].first = res % p;
	vis[i][limitN][limitM][limitK].second = res2 % p;
//	ans = (ans - res * k % p + p) % p;
//	ans = (ans + (1ll << i) % p * res % p) % p;
//	D(i);Dl(res);
	return vis[i][limitN][limitM][limitK];
}
pii bitFill(int i, int bn, int bm, bool limitN, bool limitM, bool limitK) {
	if (limitN && bitN[i] < bn) return {0,0};
	if (limitM && bitM[i] < bm) return {0,0};
	if (limitK && bitK[i] > (bn ^ bm)) return {0,0};
	pii tmp = dfs(i - 1, (bitN[i] == bn && limitN), (bitM[i] == bm && limitM), (limitK && (bn ^ bm == bitK[i])));
//	if (i == 0) Dl((1ll << i) *(bn ^ bm) % p * tmp % p);
//	ans = (ans + tmp.second + (1ll << i) * (bn ^ bm) % p * tmp.first % p) % p;
//	Dl(tmp.second);
//	Dl((1ll << i) * (bn ^ bm) % p * tmp.first % p);
	return {tmp.first,tmp.second + (1ll << i) * (bn ^ bm) % p * tmp.first % p};
}
void solve() {
	cin >> n >> m >> k >> p;
	n--;m--;
	init(n, m, k);
//	Dl(bitN[1]);
	pii tmp = dfs(62, 1, 1, 1);
//	Dl(cnt);
	ans = (ans - tmp.first * (k % p) % p + p) % p;
	ans = (ans + tmp.second % p) % p;
	cout << ans << "\n";
}
signed main() {
	//freopen(".in","r",stdin);
	//freopen(".out","w",stdout);
	cin >> t;
	while (t--) {
		solve();
	}
	return 0;
}