动态规划

Posted izlyforever

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了动态规划相关的知识,希望对你有一定的参考价值。

动态规划是研究一大类问题(特别是最值问题)的一种思路。从大二刚开始 ICPC 竞赛的时候第一次遇到,到大三学运筹学系统的了解,再到后来一直成为解决问题的一种思考方式。可以说动态规划真的是万金油的方法。

计算机领域(或者说博弈论)中的动态规划,就如同数学中的数学归纳法,一样重要。

运用动态规划的能力,就好比武侠小说中的内功,是随着时间慢慢累积的。
进阶可看 zscoder 的博文

动态规划应用举例

  • 数列的递推关系
  • 背包问题
  • 图论中 Floyd 算法,Dijkstra 算法
  • 流水线问题,旅行商问题
  • 其它杂类问题

动态规划原理

动态规划(dynamic programming)于 1950s 被 Richard Bellman 发现。引用这里的话,动态规划本质就是:

  • 定义问题的状态(必须满足"无后效性":这个就很玄学了)
  • 写出状态间的转移方程

从而递推(分治)的解决问题。

难点就在于定义问题的状态

很多时候一个变量的问题,我们需要强行加一个(甚至两个)变量来定义状态

然后给出状态转移方程,再做时间空间的优化。

常规动态规划的状态定义

  • 以该位置结尾的最优策略
  • 多设一个变量的最优策略

动态规划举例(长期更新)

codeforces1354F

\\(n\\) 张牌,\\(i\\) 号牌的数值是 \\(a_i\\),当 \\(i\\) 号牌放入桌上,之前在桌上的牌,每张数值增加 \\(b_i\\),桌上的牌可以销毁(每张牌最多销毁一次),但是桌上牌的数量不能超过 \\(k\\)。 问使桌上牌数值总和最大的放法。其中数据满足

\\[1 \\leq k \\leq n,\\; 1 \\leq a_i \\leq N,\\; 0 \\leq b_i \\leq N = 10^5, \\; a_i,b_i,n,k \\in \\mathbb{N} \\]

由于牌的编号于问题无关,所以不妨设 \\(b_i\\) 单调递增。

状态定义:dp[i][j] 表示:场上前 \\(j\\) 张牌数(编号都不超过 \\(i\\) )的桌面牌总和最大值(其实还要减去后 \\(k-j\\) 张牌的原始面值)。如果不减去就有后效性了!!!

状态转移:我们考虑第 \\(i\\) 张牌,

  • 如果将它最终留在桌面上,那么它一定是最后一张放在桌面上的,因为 \\(b_i\\) 单调递增。此时 \\(dp[i][j] = dp[i-1][j-1]+(j-1)b[i] + a[i]\\)
  • 如果它没留在桌面,那么它一定会在后来第 \\(k\\) 张牌加过 buff,\\(dp[i][j] = dp[i-1][j] + (k-1)b[i]\\)

所以

\\[dp[i][j] = \\max(dp[i-1][j-1]+(j-1)b[i]+a[i],\\; dp[i-1][j]+(k-1)b[i]) \\]

我们可以用 isin[i][j] 来标记桌上第 \\(j\\) 张牌是否是 \\(i\\) 号牌。

#include<bits/stdc++.h>
using namespace std;
const int N = 102;
int dp[N][N];
bool isin[N][N],chose[N];
using node = tuple<int,int,int>;
node q[N];
void getans(int n,int k){
	for(int i=1;i<=n;++i){
		dp[i][0] = dp[i-1][0]+(k-1)*get<2>(q[i]);
		for(int j = 1;j<i && j<=k;++j){
			int x = dp[i-1][j]+(k-1)*get<2>(q[i]);
			int y = dp[i-1][j-1]+(j-1)*get<2>(q[i])+get<1>(q[i]);
			if(x>y){
				dp[i][j] = x;
				isin[i][j] = false;
			}else{
				dp[i][j] = y;
				isin[i][j] = true;
			}
		}
		if(i<=k){
			dp[i][i] = dp[i-1][i-1]+(i-1)*get<2>(q[i])+get<1>(q[i]);
			isin[i][i] = true;
		}
	}
	for(int i=n,j=k;i;--i){
		if(isin[i][j]){
			chose[i]=true;
			--j;
		}else{
			chose[i]=false;
		}
	}
}
 
int main(){
	//freopen("in","r",stdin);
	std::ios::sync_with_stdio(false);std::cin.tie(nullptr);
	int cas,n,k,a,b;
	cin>>cas;
	while(cas--){
		cin>>n>>k;
		for(int i=1;i<=n;++i){
			cin>>a>>b;
			q[i] = {i,a,b};

		}
		sort(q+1,q+n+1,[](const node & x, const node & y){
			return get<2>(x)<get<2>(y);
		});
		cout<<(2*n-k)<<endl;
		getans(n,k);
		int last = 0;
		for(int i=1;i<=n;++i){
			if(chose[i]){
				if(++last == k){
					last = i;
					break;
				}
				cout<<get<0>(q[i])<<" ";
			}
		}
		for(int i=1;i<=n;++i){
			if(!chose[i])  cout<<get<0>(q[i])<<" "<<-get<0>(q[i])<<" ";
		}
		cout<<get<0>(q[last])<<endl;
	}
	return 0;
}

hdu 5691

这题大意:给你一些数,数值固定,部分数的位置随意调节,问你如何调解使得下式取值最大

\\[a_0 a_1 + a_1 a_2 + \\cdots + a_{n-1} a_n \\]

这确实是经典的状态转移问题:

\\(i\\) 的 2 进制表示是 \\(0 \\cdots 0 i_1 0 \\cdots 0 i_x 0\\cdots 0\\)\\(x\\) 位。
dp[i][j] 表示前 \\(x\\) 个空 分别填了 \\(a[i_1],a[i_2],\\cdots,a[i_x]\\) 的一个排列 且 \\(i_x = j\\) 使目标最大的最大值。
那么,自然地有 dp[i|(1<<k)][k] = max(dp[i][j]+a[j]*a[k])

详细转移见代码:

#include <cstdio>
template<typename T>
void upmax(T &a,T b){ if (a<b) a=b;}
const int INF =  0x3f3f3f3f;
const int N = 16;
int a[N],p[N];
int dp[1<<N][N];
int main(){
    int T, Case = 0;
    scanf("%d",&T);
    while(T--){
        printf("Case #%d:\\n",++Case);
        int n, x;
        scanf("%d",&n);
        for(int i=0;i<n;++i)    p[i] = -1;
        for(int i=0;i<n;++i){
            scanf("%d%d",a+i, &x);
            if(x != -1) p[x] = i;
        }
        for(int i=0;i<(1<<n);++i){
            for(int j=0;j<n;++j){
                dp[i][j] = -INF;
            }
        }
        if(p[0]!=-1)    dp[1<<p[0]][p[0]] = 0;
        else for(int i=0;i<n;++i)   dp[1<<i][i] = 0;
        for(int i=1;i<(1<<n);++i){
            for(int j=0;j<n;++j){
                if(!(i&(1<<j)))   continue;
                for(int k=0;k<n;++k){
                    if(i&(1<<k))    continue;
                    int x = __builtin_popcount(i);
                    if(p[x] == -1 || p[x] == k){
                        upmax(dp[i|(1<<k)][k], dp[i][j]+a[j]*a[k]);
                    }
                }
            }
        }
        int res = -INF;
        for(int i=0;i<n;++i) upmax(res, dp[(1<<n)-1][i]);
        printf("%d\\n",res);
    }
    return 0;
}

Codeforces 1312E

我们用 ans[i][j] 表示前 i 个数中以 j 结尾的最短数列长度,并用 b[i][j] 保存导致它以 j 结尾的前缀首项的位置。那么状态转移就显然,可从代码中读取。

#include<bits/stdc++.h>
using namespace std;
using LL = long long;
const int N = 1022;
const int inf = 0x3f3f3f3f;
int ans[502][N],b[502][N];
 
int main(){
	//freopen("in","r",stdin);
	std::ios::sync_with_stdio(false);std::cin.tie(nullptr);
	int n,x;
	while(cin>>n){
		memset(ans,inf,sizeof(ans));
		cin>>x;
		ans[0][x] = 1;
		b[0][x]=0;
		for(int i=1;i<n;++i){
			cin>>x;
			for(int j=1;j<N;++j){
				if(ans[i-1][j]!=inf){
					ans[i][x] = min(ans[i][x],ans[i-1][j]+1);
				}
				b[i][x]=i;
			}
			int s = i-1;
			while(s>=0&&ans[s][x]!=inf){
				ans[i][x+1] = ans[s][x];
				b[i][x+1] = b[s][x];
				s = b[s][x]-1;
				++x;
			}
		}
		int r = inf;
		for(int i=1;i<N;++i){
			r = min(r,ans[n-1][i]);
		}
		cout<<r<<endl;
	}
	return 0;
}

洛谷 P1005 矩阵取数游戏

每一行单独考虑,然后因为数据范围,所以用 Python 交题

按照规模做 dp,最近做的很少,记录一哈 2020/7/4

def f(a):
	b = a.copy()
	for i in range(1,len(a)):
		c = []
		for j in range(0,len(b)-1):
			c.append(max(2*b[j]+a[i+j],2*b[j+1]+a[j]))
		b=c
	return 2*b[0]

n = int(input().split()[0])
r=0
for i in range(n):
	r += f([int(_) for _ in input().split()])
print(r)

洛谷 T135246 「SWTR-05」Grid

dp[i][j] :第 i 行,以第 j 个数结尾的最小和, s[i][j] :第 i 行,前 j 个数之和

rr[i][j] :到达 i,j 位置经过的数字和。

r[i][j] :首次到达 i,j 位置经过格子的数字和(不包括 i,j 的值)初值为 rr[i+1][j]

dp[i][j]s[i][j] 的状态转移是显然的,rr[i][j]r[i][j] 状态转移:

\\[\\begin{aligned} r[i][j] = \\min_{j \\leq k<m} r[i][k] + s[i][k] - s[i][j] \\\\ rr[i][j] = r[i][j] + dp[i][j] \\end{aligned} \\]

由于 r[i][j] 转移式的对称性,我们考虑记录 s[i][j] + r[i][j] 的最小值,(直接优化了一个 \\(O(m)\\)),这也是此题的经典之处。最后 rr 数组可被优化掉。当然也可以对 r 进行空间优化,但是没必要

#include<bits/stdc++.h>
using namespace std;
using LL = long long;

int main(){
	//freopen("in","r",stdin);
	std::ios::sync_with_stdio(false);std::cin.tie(nullptr);
	int n,m;
	cin>>n>>m;
	LL a[n][m],dp[n][m],s[n][m],r[n+1][m];
	for(int i=0;i<n;++i){
		int cur = 0;
		for(int j=0;j<m;++j){
			cin>>a[i][j];
			if(j==0) s[i][j] = a[i][j];
			else s[i][j] = s[i][j-1]+a[i][j];
			cur += a[i][j];
			dp[i][j] = cur;
			cur = min(cur,0);
		}
	}
	for(int i=n-1;i>=0;--i){
		LL mi = 1e9+2;
		for(int j=m-1;j>=0;--j){
			r[i][j] = (i==n-1?0:r[i+1][j]);
			if(r[i][j]<mi-s[i][j]){
				mi = r[i][j]+s[i][j]; 
			}else{
				r[i][j] = mi-s[i][j];
			}
		}
		for(int j=0;j<m;++j){
			r[i][j] += dp[i][j];
		}
	}
	LL ret = r[0][0];
	for(int j=1;j<m;++j){
		ret = min(ret,r[0][j]);
	}
	cout<<ret<<endl;
	return 0;
}

下面上题的 空间优化版本(优雅的不行,需要考虑从上向下走)

rr[j] 表示上一行的答案, r[j] 表示当前行的答案。那么 r[j] 的初始值应该为 rr[j]+dp[j],状态转移

\\[r[j] = \\min_{0 \\leq k \\leq j} r[k] - s[k] + s[j] \\]

#include<bits/stdc++.h>
using namespace std;
using LL = long long;

int main(){
	//freopen("in","r",stdin);
	std::ios::sync_with_stdio(false);std::cin.tie(nullptr);
	int n,m;
	cin>>n>>m;
	LL a[m],s[m],dp[m],r[m],rr[m]={};
	for(int i=0;i<n;++i){
		LL cur = 0;
		for(int j=0;j<m;++j){
			cin>>a[j];
			cur += a[j];
			dp[j] = cur;
			cur = min(cur,0LL);
			s[j] = a[j]+(j==0?0:s[j-1]);
		}
		cur = rr[0]+dp[0]-s[0];
		for(int j=0;j<m;++j){
			r[j] = min(rr[j]+dp[j],cur+s[j]);
			cur = min(cur,r[j]-s[j]);
		}
		for(int j=0;j<m;++j) rr[j] = r[j];
	}
	cout<<*min_element(rr,rr+m)<<endl;
	return 0;
}

T134867 [YsOI2020]归零

#include<bits/stdc++.h>
using namespace std;
using LL = long long;
const LL M = 998244353;
const int N = 132;
LL f[N],invf[N];
LL pow_mod(LL x,LL n){
	LL r = 1;
	while(n){
		if(n&1) r=r*x%M;
		n>>=1; x=x*x%M;
	}
	return r;
}
void init(){
	f[0]=1;
	for(int i=1;i<N;++i) f[i]=f[i-1]*i%M;
	invf[N-1] = pow_mod(f[N-1],M-2);
	for(int i=N-2;i>=0;--i) invf[i] = invf[i+1]*(i+1)%M;
}
LL C(int n,int k){
	if(k>n) return 0;
	return f[n]*invf[n-k]%M*invf[k]%M;
}
int main(){
	//freopen("in","r",stdin);
	std::ios::sync_with_stdio(false);std::cin.tie(nullptr);
	init();
	string ss;
	cin>>ss;
	vector<int> s={0};
	for(auto &c:ss) s.emplace_back(c-\'0\');
	auto plusOne = [&](int l,int r){
		int id = r-1;
		while(s[id]==9) --id;
		++s[id];
		for(int i=id+1;i<r;++i) s[i] = 0;
	};
	auto minusOne = [&](int l,int r){
		int id = r-1;
		while(s[id]==0) --id;
		--s[id];
		for(int i=id+1;i<r;++i) s[i] = 9;
	};
	function<map<int,LL>(int,int)> f = [&](int l,int r) -> map<int,LL>{
		if(all_of(s.begin()+l,s.begin()+r,[](int x){ return x == 0;})){
			return map<int,LL>{{0,1}};
		}
		map<int,LL> ret;
		for(int i=l;i<r;++i){
			if(s[i]){
				int x = s[i];
				s[i]=0;
				if(x>4) plusOne(l,i);
				for(auto &ia:f(l,i)){
					for(auto &ib:f(i,r)){
						int t1 = 1+ia.first+ib.first;
						LL t2 = ia.second*ib.second%M*C(ia.first+ib.first,ia.first)%M;
						if(ret.find(t1)==ret.end()){
							ret[t1] = t2;
						}else ret[t1] = (ret[t1]+t2)%M;
					}
				}
				if(x>4) minusOne(l,i);
				s[i]=x;
			}
		}
		return ret;
	};
	LL res = 0;
	for(auto &x:f(0,s.size())){
		//cout<<x.first<<" "<<x.second<<endl;
		res+=x.second;
	}
	cout<<res%M<<endl;
	return 0;
}

状态压缩 DP(mask dp)

给定 \\(2n\\) 个非负整数,求两两配对的最大公约数的和的最大值

#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
#define print(x) std::cout << (x) << std::endl
#define println std::cout << std::endl
using LL = long long;

int main() {
	//freopen("in","r",stdin);
	std::ios::sync_with_stdio(false);
	std::cin.tie(nullptr);

	// input n and 2n non-negetive integers(less than 1e9)
	int n;
	std::cin >> n;
	n *= 2;
	std::vector<int> a(n);
	for (auto &x : a) std::cin >> x;
	// initial all gcd which will be used for effectiveness
	std::vector<std::vector<int>> gcd(n, std::vector<int>(n));
	for (int i = 1; i < n; ++i) {
		for (int j = 0; j < i; ++j) {
			gcd[i][j] = std::__gcd(a[i], a[j]);
		}
	}
	
	auto cmax = [](LL &a, LL b) {
		if (a < b) a = b;
	};
	// total complex O(2^n * n^3) and we can remove the "if" part of following code
	std::vector<LL> mask(1 << n);
	for (int step = 0; step < n; ++step) { // every step choose two integers
		for (int msk = 0; msk < (1 << n); ++msk) if (__builtin_popcount(msk) == 2 * step) {
			for (int i = 1; i < n; ++i) if (!(msk & (1 << i))) {
				for (int j = 0; j < i; ++j) if (!(msk & (1 << j))) {
					cmax(mask[msk|(1 << i)|(1 << j)], mask[msk] + gcd[i][j]);
				}
			}
		}
	}

	print(mask.back());
	
	return 0;
}

本问题可以在多项式时间用一般图匹配 解决。

旅行商问题

#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
#define print(x) std::cout << (x) << std::endl
#define println std::cout << std::endl
using LL = long long;
 
int main() {
	//freopen("in","r",stdin);
	std::ios::sync_with_stdio(false);
	std::cin.tie(nullptr);
	int n;
	std::cin >> n;
	std::vector<std::tuple<int, int, int>> a(n);
	for (auto &[x, y, z] : a) std::cin >> x >> y >> z;
	auto dist = [&](int i, int j) {
		auto [aix, aiy, aiz] = a[i];
		auto [ajx, ajy, ajz] = a[j];
		return abs(aix - ajx) + abs(aiy - ajy) + std::max(0, ajz - aiz);
	};
	std::vector<std::vector<int>> d(n, std::vector<int>(n));
	for (int i = 0; i < n; ++i) {
		for (int j = 0; j < n; ++j) {
			d[i][j] = dist(i, j);
		}
	}
	auto cmin = [](int &a, int b) {
		if (a > b) a = b;
	};
	std::vector<std::vector<int>> mask(1 << n, std::vector<int>(n, 1e8));
	mask[1][0] = 0;
	for (int step = 1; step < n; ++step) {
		for (int msk = 1; msk < (1 << n); msk += 2) if (__builtin_popcount(msk) == step) {
			for (int i = 0; i < n; ++i) if (msk & (1 << i)) {
				for (int j = 1; j < n; ++j) if (!(msk & (1 << j))) {
					cmin(mask[msk|(1 << j)][j], mask[msk][i] + d[i][j]);
				}
			}
		}
	}
	int r = 1e8;
	for (int i = 1; i < n; ++i) r = std::min(r, mask[(1 << n) - 1][i] + d[i][0]);
	print(r);
	return 0;
}

453B:状态压缩 DP

给定数列 \\(a\\),求满足元素两两互素的数列 \\(b\\) 使得 \\(\\sum |a_i - b_i|\\) 最小

注意到 \\(b_i < 2 a_i\\),因为否则取 \\(b_i = 1\\) 即可。

由于 \\(60\\) 内的素数个数为 17, 因此可以状态压缩 DP。设 dp[i][j] 表示使得 \\(\\sum_{k = 1} ^ i |a_k - b_k|\\) 最小,且\\(b_1 \\cdots b_i\\) 中所有出现的素因子的状态为 \\(j\\)。因此状态转移就是 dp[i][j | factor[k]] = min(dp[i - 1][j] + |a_i - k|),其中 factor[k]\\(j\\) 没有交集。

#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
#define print(x) std::cout << (x) << std::endl
#define println std::cout << std::endl
using LL = long long;

int main() {
	//freopen("in","r",stdin);
	std::ios::sync_with_stdio(false);
	std::cin.tie(nullptr);
	int n;
	std::cin >> n;
	std::vector<int> a(n);
	for (auto &x : a) std::cin >> x;
	int ma = *std::max_element(a.begin(), a.end()) * 2;
	std::vector<int> p;
	for (int i = 2; i < ma; ++i) {
		bool flag = true;
		for (int j = 2; j * j <= i; ++j) if (i % j == 0) {
			flag = false;
			break;
		}
		if (flag) p.emplace_back(i);
	}
	std::vector<int> factor(ma);
	for (int i = 2; i < ma; ++i) {
		for (int j = 0; j < p.size(); ++j) if (i % p[j] == 0) {
			factor[i] |= (1 << j);
		}
	}
	std::vector<std::vector<int>> ans(n + 1, std::vector<int>(1 << p.size(), 1e9));
	ans[0][0] = 0;
	for (int i = 1; i <= n; ++i) {
		for (int j = 1; j < 2 * a[i - 1]; ++j) {
			for (int k = 0; k < (1 << p.size()); ++k) if ((k & factor[j]) == 0) {
				ans[i][k | factor[j]] = std::min(ans[i][k | factor[j]], ans[i - 1][k] + abs(a[i - 1] - j));
			}
		}
	}
	std::vector<int> r(n);
	int now = std::min_element(ans[n].begin(), ans[n].end()) - ans[n].begin();
	for (int i = n - 1; i >= 0; --i) {
		for (int j = 1; j < 2 * a[i]; ++j) if ((now | factor[j]) == now) {
			if (ans[i][now ^ factor[j]] + abs(a[i] - j) == ans[i + 1][now]) {
				r[i] = j;
				now ^= factor[j];
				break;
			}
		}
	}
	for (auto x : r) std::cout << x << " ";
	println;
	return 0;
}

662C:状态压缩 DP + FWT 模板

给定 \\(n \\times m\\) 的 0-1 方阵,可以取反一些行和列使得最后 0 的数列最小。

首先注意到 \\(n < 20\\),我们可以把每一列看作一个状态 i ,并且结果跟列的顺序无关。我们可以记录下初始情况每种状态数 C[i] 量。
并且每一种状态 i 对答案的贡献显然就是它的 0, 1 个数的最小值记作 g[i]
对于每一个行取反 S, 其实就是将一个状态 i 变成 状态 i ^ S
所以每一种行取反 S,最终的答案 \\(\\displaystyle F(S) = \\sum_{i} C[i] \\cdot g[i \\wedge S] = \\sum_{i \\wedge j = S} C[i] \\cdot g[j]\\)

#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
#define print(x) std::cout << (x) << std::endl
#define println std::cout << std::endl
using LL = long long;

#include <cstdio>
#include <algorithm>
#include <vector>

const int P = 998244353;

void add(int &x, int y) {
    (x += y) >= P && (x -= P);
}
void sub(int &x, int y) {
    (x -= y) < 0 && (x += P);
}
struct FWT {
    int extend(int n) {
        int N = 1;
        for (; N < n; N <<= 1);
        return N;
    }
    void FWTor(std::vector<int> &a, bool rev) {
        int n = a.size();
        for (int l = 2, m = 1; l <= n; l <<= 1, m <<= 1) {
            for (int j = 0; j < n; j += l) for (int i = 0; i < m; i++) {
                if (!rev) add(a[i + j + m], a[i + j]);
                else sub(a[i + j + m], a[i + j]);
            }
        }
    }
    void FWTand(std::vector<int> &a, bool rev) {
        int n = a.size();
        for (int l = 2, m = 1; l <= n; l <<= 1, m <<= 1) {
            for (int j = 0; j < n; j += l) for (int i = 0; i < m; i++) {
                if (!rev) add(a[i + j], a[i + j + m]);
                else sub(a[i + j], a[i + j + m]);
            }
        }
    }
    void FWTxor(std::vector<int> &a, bool rev) {
        int n = a.size(), inv2 = (P + 1) >> 1;
        for (int l = 2, m = 1; l <= n; l <<= 1, m <<= 1) {
            for (int j = 0; j < n; j += l) for (int i = 0; i < m; i++) {
                int x = a[i + j], y = a[i + j + m];
                if (!rev) {
                    a[i + j] = (x + y) % P;
                    a[i + j + m] = (x - y + P) % P;
                } else {
                    a[i + j] = 1LL * (x + y) * inv2 % P;
                    a[i + j + m] = 1LL * (x - y + P) * inv2 % P;
                }
            }
        }
    }
    std::vector<int> Or(std::vector<int> a1, std::vector<int> a2) {
        int n = std::max(a1.size(), a2.size()), N = extend(n);
        a1.resize(N), FWTor(a1, false);
        a2.resize(N), FWTor(a2, false);
        std::vector<int> A(N);
        for (int i = 0; i < N; i++) A[i] = 1LL * a1[i] * a2[i] % P;
        FWTor(A, true);
        return A;
    }
    std::vector<int> And(std::vector<int> a1, std::vector<int> a2) {
        int n = std::max(a1.size(), a2.size()), N = extend(n);
        a1.resize(N), FWTand(a1, false);
        a2.resize(N), FWTand(a2, false);
        std::vector<int> A(N);
        for (int i = 0; i < N; i++) A[i] = 1LL * a1[i] * a2[i] % P;
        FWTand(A, true);
        return A;
    }
    std::vector<int> Xor(std::vector<int> a1, std::vector<int> a2) {
        int n = std::max(a1.size(), a2.size()), N = extend(n);
        a1.resize(N), FWTxor(a1, false);
        a2.resize(N), FWTxor(a2, false);
        std::vector<int> A(N);
        for (int i = 0; i < N; i++) A[i] = 1LL * a1[i] * a2[i] % P;
        FWTxor(A, true);
        return A;
    }
} fwt;

int main() {
	//freopen("in","r",stdin);
	std::ios::sync_with_stdio(false);
	std::cin.tie(nullptr);
	int n, m;
	std::cin >> n >> m;
	std::vector<std::string> a(n);
	for (auto &x : a) std::cin >> x;
	std::vector<int> c(1 << n), g(1 << n);
	for (int i = 0; i < m; ++i) {
		int r = 0;
		for (int j = 0; j < n; ++j) {
			r |= (a[j][i] - \'0\') << j;
		}
		++c[r];
	}
	for (int i = 0; i < n; ++i) {
		for (int j = 0; j < (1 << n); ++j) {
			if (j & (1 << i)) ++g[j];
		}
	}
	for (int i = 0; i < (1 << n); ++i) {
		g[i] = std::min(g[i], n - g[i]);
	}
	auto f = fwt.Xor(c, g);
	print(*std::min_element(f.begin(), f.end()));
	return 0;
}

树形 DP

树形 DP 本质上就是,树结构可以给出各种不同的偏序关系,针对不同的问题,给出偏序关系,再来 DP

换根 DP

树形 DP 的进阶:朝夕 ACM 笔记

模板例题:LOJ P3478

题意:求以树的某个节点为根,各个节点的深度之和

做法:先以 1 为根预处理出所有子树的深度之和,然后 DP,例如 v 的父节点为 u,那么 ans[v] = ans[u] - sz[v] + sz[1] - sz[v] (以 v 的根的子树的深度都要降低 1, v 的父节点和兄弟节点深度都要加 1)

#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
using LL = long long;

int main() {
	//freopen("in", "r", stdin);
	std::ios::sync_with_stdio(false);
	std::cin.tie(nullptr);
	int n;
	std::cin >> n;
	std::vector<std::vector<int>> e(n + 1);
	for (int i = 1; i < n; ++i) {
		int u, v;
		std::cin >> u >> v;
		e[u].emplace_back(v);
		e[v].emplace_back(u);
	}
	std::vector<int> dep(n + 1), sz(n + 1);
	std::vector<LL> ans(n + 1);
	std::function<void(int, int)> pdfs = [&](int u, int fa) -> void {
		sz[u] = 1;
		ans[u] = dep[u];
		for (auto v : e[u]) if (v != fa) {
			dep[v] = dep[u] + 1;
			pdfs(v, u);
			sz[u] += sz[v];
			ans[u] += ans[v];
		}
	};
	dep[1] = 0;
	pdfs(1, 0);
	// 预处理出 1 为根的结果,然后进行换根 DP
	std::function<void(int, int)> dfs = [&](int u, int fa) -> void {
		for (auto v : e[u]) if (v != fa) {
			ans[v] = ans[u] + sz[1] - 2 * sz[v];
			dfs(v, u);
		}
	};
	dfs(1, 0);
	std::cout << std::max_element(ans.begin(), ans.end()) - ans.begin() << std::endl;
	return 0;
}

换根 DP 套路:

  • dfs 预处理以 1 为根的结果,预处理时其实也计算了子树的结果
  • 第二次 dfs 进行 DP。

其它例题:1324F708C

斜率优化 DP(\\(O(n)\\)

形如 \\(\\displaystyle dp[i] = c_i + \\min_{j < i} a_j x_i + b_j\\) 的都可以用斜率优化 DP(要求 \\(x_i, a_j\\) 单调,最大值同理也是一样的)

也称凸壳优化 DP

例题:LOJ P2120

就此题具体说明斜率优化 DP

首先设 dp[i] 为在第 i 个工厂建设仓库,前 i 个工厂的费用之和的最小值。显然 \\(dp[0] = 0\\)

\\[dp[i] = c_i + \\min_{j < i} dp[j] + \\sum_{k = j + 1}^i (x_i - x_k) p_k \\]

\\(a_i = \\sum_{j = 1}^i p_j\\)\\(b_i = \\sum_{j = 1}^i x_j p_j\\),则

\\[dp[i] = c_i + a_i x_i - b_i + \\min_{j < i} -a_j x_i + (b_j + dp[j]) \\]

不妨设 \\(i > j > k\\),那么若决策 j 优于 k 当且仅当 \\(-a_j x_i + b_j + dp[j] < -a_k x_i + b_k + dp[k]\\) 当且仅当 \\(x_i > \\frac{(b_j + dp[j]) - (b_k + dp[k])}{a_j - a_k}\\)(记作 solpe(j, k)),显然若 solpe(i, j) < solpe(j, k)j 必不可能成为最优点。因此相邻的节点斜率必然是递增的,因此合法队首对应的就是答案。用一个双向队列即可。(实现的时候变量有一些混用为了代码的简洁)

#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
using LL = long long;

int main() {
	// freopen("C:\\\\Users\\\\dna049\\\\cf\\\\in", "r", stdin);
	std::ios::sync_with_stdio(false);
	std::cin.tie(nullptr);
	int n;
	std::cin >> n;
	std::vector<LL> x(n + 1), a(n + 1), c(n + 1), b(n + 1), dp(n + 1);
	for (int i = 1; i <= n; ++i) {
		std::cin >> x[i] >> a[i] >> c[i];
		++x[i]; // 为了避免 x[0] = x[1]
	}
	for (int i = 1; i <= n; ++i) {
		b[i] = b[i - 1] + x[i] * a[i];
		a[i] += a[i - 1];
	}
	// j > k, 
	auto solpe = [&](int j, int k) -> double {
		return double(b[j] - b[k]) / (a[j] - a[k]); 
	};
	// 这里手写双向队列要更为方便,队列中相邻两点斜率是单调递增的,且队首最优
	std::vector<int> Q(n + 1);
	int l = 0, r = 0;
	for (int i = 1; i <= n; ++i) {
		while (l < r && solpe(Q[l + 1], Q[l]) <= x[i]) ++l;
		dp[i] = c[i] + (a[i] - a[Q[l]]) * x[i] - b[i] + b[Q[l]];
		b[i] += dp[i];
		while (l < r && solpe(Q[r], Q[r - 1]) >= solpe(i, Q[r])) --r;
		Q[++r] = i;
	}
	std::cout << dp.back() << "\\n";
	return 0;
}

注意到上述问题中 \\(b_j\\) 的取值和 dp[j] 有关,即不能提前给出 \\(b_j\\),因此没法写出可复用的代码,必须好好理解,然后灵活运用

因此我们更一般性的利用 凸包优化 DP\\(O(n \\log n)\\) 复杂度更一般性的解决问题,不需要通用性

凸包优化 DP(\\(O(n \\log n)\\)

四边形优化 DP

区间 2D1D 动态规划,将 \\(O(n^3)\\) 复杂度优化成 \\(O(n^2)\\)

\\[f_{l, r} = \\min_{l \\leq k < r} f_{l, k} + f_{k + 1, r} + w(l, r) \\qquad (1 \\leq l < r \\leq n) \\]

其中 \\(w(l, r)\\) 满足

  • 区间单调性:\\(w(l\', r\') \\leq w(l, r)\\) 对任意 \\(l \\leq l\' \\leq r\' \\leq r\\) 成立
  • 四边形不等式:\\(w(l_1, r_1) + w(l_2, r_2) \\leq w(l_2, r_1) + w(l_1, r_2)\\) 对任意 \\(l_1 \\leq l_2 \\leq r_1 \\leq r_2\\) 成立。
  • 四边形不等式(等价形式):\\(w(i, j) + w(i + 1, j + 1) \\geq w(i + 1, j) + w(i, j + 1)\\) 对任意 \\(i < j\\) 成立。

\\(w(l, r)\\) 满足上述关系,那么 \\(f_{l, r}\\) 满足四边形不等式。首先注意到 \\(l_1 = l_2\\)\\(r_1 = r_2\\) 时是平凡的。我们分 \\(l_1 < l_2 = r_1 < r_2\\)\\(l_1 < l_2 < r_1 < r_2\\) 两种情况讨论,都用数学归纳法证明。第一种情形只需 \\(w(l, r)\\) 的区间单调性,第二种情形的证明只需四边形不等式。

完整证明

此时记 \\(m_{l, r} = \\min \\{k: f_{l, r} = f_{l, k} + f_{k + 1, r} + w(l, r)\\} \\quad (1 \\leq l < r \\leq n)\\) 即最小最优决策点。则有(反证法证明)

\\[m_{l, r - 1} \\leq m_{l, r} \\leq m_{l + 1, r} \\quad (l + 1 < r) \\]

因此如果我们在计算 \\(f_{l, r}\\) 的同时记录下最小最优决策点 \\(m_{l, r}\\) 那么我们对 决策点 \\(k\\) 的总枚举次数为

\\[\\sum_{1 \\leq l \\leq r} m_{l, r} = n - 1 + \\sum_{1 \\leq l + 1 \\leq r} m_{l + 1, r} - m_{l, r -1} = n - 1 \\sum_{1 < i < n} m_{i, n} - m_{1, i} \\leq n^2 \\]

\\(w\\) 仅满足区间单调性,我们取最大值,那么做法完全不同,此时

\\[g_{l, r} = \\max_{l \\leq k < r} g_{l, k} + g_{k + 1, r} + w(l, r) \\qquad (1 \\leq l < r \\leq n) \\]

我们可以证明:\\(g_{l, r} = \\max \\left(g_{l, l} + g_{l + 1, r}, g_{l, r - 1} + g(r, r) \\right) + w(l, r)\\)

\\(k\\)\\(g_{l, r}\\) 的最小最优决策点。不妨假设 \\(l < k < r - 1\\),设 \\(u\\)\\(g_{l, k}\\) 的最优决策点,\\(v\\)\\(g_{k + 1, r}\\) 的最优决策点。那么我们先决策 \\(u\\) 再决策 \\(k\\) 可知 \\(w(u + 1, r) < w(l, k)\\)。先决策 \\(v\\) 再决策 \\(k\\)\\(w(l, v) \\leq w(k + 1, r)\\),然而 \\(w(l, v) + w(u + 1,r) \\geq w(l, k) + w(k + 1, r)\\) 矛盾。(画图看就能看得很清晰)

模板例题:LOJ P1880

题意:在一个圆圈上,有 n 堆石子,可以合并相邻的石子获得合并后的石子数的得分。问最后合并成一堆时最大得分和最小得分。

做法:首先我们可以搞 2n 堆石子,破圈为链。然后设 \\(f_{i, j}, g_{i, j}\\) 为分别为第 i 堆到第 j 堆合并成一堆的最小和最大得分。记 \\(w(i, j)\\) 为第 i 堆到第 j 堆石子数之和,则

\\[\\begin{aligned} f_{i, j} = \\min_{i \\leq k < j} g(i, k) + g(k + 1, j) + w(i, j) \\\\ g_{i, j} = \\max_{i \\leq k < j} g(i, k) + g(k + 1, j) + w(i, j) \\end{aligned} \\]

显然 \\(w(i, j)\\) 满足区间单调性和四边形恒等式,因此对于最小都可以用平行四边形优化。

对于最大值,用上述优化做法。

#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
using LL = long long;

int main() {
	// freopen("C:\\\\Users\\\\dna049\\\\cf\\\\in", "r", stdin);
	std::ios::sync_with_stdio(false);
	std::cin.tie(nullptr);
	int n;
	std::cin >> n;
	std::vector<LL> a(2 * n + 1);
	// 破圈为链
	for (int i = 1; i <= n; ++i) {
		std::cin >> a[i];
		a[i + n] = a[i];
	}
	for (int i = 1; i <= 2 * n; ++i) a[i] += a[i - 1];
	auto w = [&](int l, int r) {
		return a[r] - a[l - 1];
	};
	std::vector<std::vector<LL>> f(2 * n + 1, std::vector<LL>(2 * n + 1)), mf(2 * n + 1, std::vector<LL>(2 * n + 1));
	for (int i = 1; i < 2 * n; ++i) {
		f[i][i + 1] = w(i, i + 1);
		mf[i][i + 1] = i;
	}
	auto g = f;
	for (int len = 2; len < n; ++len) {
		for (int l = 1, r = len + 1; r <= 2 * n; ++l, ++r) {
			f[l][r] = INT64_MAX;
			for (int k = mf[l][r - 1]; k <= mf[l + 1][r]; ++k) {
				if (f[l][r] > f[l][k] + f[k + 1][r]) {
					f[l][r] = f[l][k] + f[k + 1][r];
					mf[l][r] = k;
				}
			}
			f[l][r] += w(l, r);
			g[l][r] = std::max(g[l + 1][r], g[l][r - 1]) + w(l, r);			
		}
	}
	LL ff = INT64_MAX;
	for (int i = 1; i <= n; ++i) ff = std::min(ff, f[i][i + n - 1]);
	LL gg = INT64_MIN;
	for (int i = 1; i <= n; ++i) gg = std::max(gg, g[i][i + n - 1]);
	std::cout << ff << "\\n" << gg << "\\n";
	return 0;
}

石子合并问题的最优解法是 GarsiaWachs 算法,复杂度 \\(O(n \\log n)\\)。例题:LOJ 5569,以后有时间再学论文:A New Proof of the Garsia-Wachs Algorithm,本质是决策树问题。

二维滚动 DP 分治 DP 将 \\(O(n m^2)\\) 复杂度优化成 \\(O(n m)\\)

\\[f_{i, j} = \\min_{k < j} f_{i - 1, k} + w(k + 1, j) \\quad (1 \\leq i \\leq n, 1 \\leq j \\leq m) \\]

其中 \\(f(0, 0) = 0\\)\\(f_0, i = \\infty, f_{i, j} = 0 \\quad i > j\\)\\(w\\) 满足区间单调和四边形不等式。

不难看出 \\(f_{1, j} = w(1, j)\\)

我们对 \\(i\\) 数学归纳法证明:\\(f_{i, j + 1} + f_{i + 1, j} \\geq f_{i, j} + f_{i + 1, j + 1}\\) 对任意 \\(i < j\\) 成立。即证明四边形不等式。

\\(i = 1\\) 时,\\(f_{2, j} = f_{1, p} + w(p + 1, j) = w(1, p) + w(p + 1, j)\\),并且 \\(f_{2, j + 1} \\leq w(1, p) + w(p + 1, j + 1)\\)

从而

\\[f_{1, j + 1} + f_{2, j} = w(1, j + 1) + w(1, p) + w(p + 1, j) \\geq w(1, j) + w(p + 1, j + 1) + w(1, p) \\geq f_{1, j} + f_{2, j + 1} \\]

假设 \\(f_{i - 1, j} + f_{i, j - 1} \\geq f_{i - 1, j - 1} + f_{i, j}\\) 对任意 \\(i < j\\) 成立,那么显然 \\(f_{i - 1, k} + f_{i, j - 1} \\geq f_{i - 1, j - 1} + f_{i, k}\\) 对任意 \\(i < j \\leq k\\) 成立。

分情况讨论,记 \\(u, v\\) 分别为 \\(f_{i, j + 1}, f_{i+ 1, j}\\) 的最优决策点(\\(i - 1 \\leq u \\leq j, i \\leq v < j\\))。那么

假设 \\(i < i + 1 < j < j +1\\)

  • \\(u \\geq v\\),则 \\(i \\leq u \\leq j\\)\\(f_{i + 1, j + 1} \\leq f_{i, u} + w(u + 1, j +1)\\)\\(f_{i, j} \\leq f_{i - 1, v} + w(v + 1, j)\\) 从而

    \\[f_{i, j + 1} + f_{i + 1, j} = f_{i - 1, u} + w(u + 1, j +1) + f_{i, v} + w(v + 1, j) \\]

    由归纳法知道 \\(f_{i - 1, u} + f_{i, v} \\geq f_{i - 1, v} + f_{i, u} \\quad (i - 1 < i < v \\leq u)\\)。两式相加得到我们要证的目标。

  • \\(u < v\\)\\(i - 1 \\leq u < j\\)\\(f_{i + 1, j + 1} \\leq f_{i, v} + w(v + 1, j + 1), f_{i, j} \\leq f_{i - 1, u} + w(u + 1, j)\\),从而 \\(u + 1 \\leq v + 1 \\leq j \\leq j + 1\\)

    \\[f_{i, j + 1} + f_{i + 1, j} - f_{i +1, j + 1} - f_{i, j} \\geq w(u + 1, j +1) + w(v + 1, j) - w(v + 1, j + 1) - w(u + 1, j) \\geq 0 \\]

假设 \\(i < i + 1 = j < j + 1\\)

  • \\(u < j\\),则 \\(f_{i, j} \\leq f_{i - 1, u} + w(u + 1, j)\\)

    \\[f_{i, j} + f_{i + 1, j + 1} \\leq f_{i - 1, u} + w(u + 1, j) + f_{i, i} + w(i + 1, j + 1) \\leq f_{i - 1, u} + w(u + 1, j + 1) + f_{i, i} + w(i + 1, j) = f_{i, j + 1} + f_{i + 1, j} \\]

  • \\(u = j\\) ,注意到 \\(i - 1 < i = j - 1 < j\\),归纳法有 \\(f_{i - 1, j} + f_{i, j - 1} \\geq f_{i - 1, j - 1} + f_{i, j}\\)。所以 \\(f_{i, j + 1} + f_{i + 1, j} = f_{i - 1, j} + w(j + 1, j + 1) + f_{i, j - 1} + w(j, j) \\geq f_{i - 1, j - 1} + w(j, j) f_{i, j} + w(j + 1, j + 1) \\geq f_{i, j} + f_{i + 1, j + 1}\\),证毕。

\\(k[i][j]\\)\\(f_{i, j}\\) 的最小最优策略。

决策优化不等式:\\(k[i -1][j] \\leq k[i][j] \\leq k[i][j + 1]\\)(注意这与之前的不同)。

证明:记 \\(u = k[i][j], x = k[i - 1][j], y = k[i][j + 1]\\),反证法:若 \\(u < x\\)。首先由定义

\\[f_{i - 1, u} + w(u + 1, j) \\leq f_{i - 1, x} + w(x + 1, j) \\]

\\(i - 2 < i - 1 \\leq u < k\\),因此

\\[f_{i - 2, u} + f_{i - 1, x} \\leq f_{i - 2, x} + f_{i - 1, u} \\]

两式相加得到

\\[f_{i - 2, u} + w(u + 1, j) \\leq f_{i - 2, x} + w(x + 1, j) \\]

矛盾与 \\(x\\)\\(f_{i - 1, j}\\) 的最小最优策略。

同理,假设 \\(u > y\\)。由定义

\\[f_{i - 1, y} + w(y + 1, j + 1) \\leq f_{i - 1, u} + w(u + 1, j + 1) \\]

\\(y + 1 < u + 1 \\leq j < j + 1\\)

\\[w(y + 1, j) + w(u + 1, j + 1) \\leq w(y + 1, j + 1) + w(u + 1, j) \\]

两式相加得到

\\[f_{i - 1, y} + w(y + 1, j) \\leq f_{i - 1, u} + w(u + 1, j) \\]

复杂度:

\\[\\sum_{i = 1}^{n} \\sum_{j = 1}^m k_{i, j + 1} - k_{i - 1, j} = \\sum_{i = 1}^n k_{i, m + 1} + \\sum_{j = 1}^m k_{n, j} < 2 n m \\]

例题:LOJ P4767

不难看出 \\(w(k, j)\\) 表示在 \\(k\\)\\(j\\) 个村庄建一个邮局使得总距离之和最小(那肯定是建在中位数位置)不难证明它满足四边形不等式(分奇偶很好证明)。首先把 w 给预处理出来。然后再转移。

#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
using LL = long long;

int main() {
	// freopen("C:\\\\Users\\\\dna049\\\\cf\\\\in", "r", stdin);
	std::ios::sync_with_stdio(false);
	std::cin.tie(nullptr);
	int n, m;
	std::cin >> n >> m;
	std::vector<int> a(n + 1);
	for (int i = 1; i <= n; ++i) std::cin >> a[i];
	std::vector<std::vector<int>> w(n + 1, std::vector<int>(n + 1)), f(m + 1, std::vector<int>(n + 2));
	for (int i = 1; i < n; ++i) w[i][i + 1] = a[i + 1] - a[i];
	for (int len = 2; len < n; ++len) {
		for (int i = 1, j = len + 1; j <= n; ++j, ++i) {
			w[i][j] = w[i + 1][j - 1] + a[j] - a[i];
		}
	}
	auto mf = f;
	// mf[i - 1][j] \\leq mf[i][j] \\leq m[i][j + 1]
	for (int i = 1; i < n; ++i) f[0][i] = 1e9;
	for (int i = 1; i <= m; ++i) {
		mf[i][n + 1] = n;
		for (int j = n; j > 0; --j) {
			f[i][j] = INT_MAX;
			for (int k = std::max(i - 1, mf[i - 1][j]); k < j && k <= mf[i][j + 1]; ++k) {
				if (f[i][j] > f[i - 1][k] + w[k + 1][j]) {
					f[i][j] = f[i - 1][k] + w[k + 1][j];
					mf[i][j] = k;
				}
			}
		}
	}
	std::cout << f[m][n] << "\\n"; 
	return 0;
}

其它例题:321E

1D1D 四边形 + 分治 DP 将 \\(O(n^2)\\) 复杂度优化成 \\(O(n \\log n)\\)

\\[f_r = \\min_{l = 1}^{r - 1} f_l + w(l, r) \\quad (1 \\leq r \\leq n) \\]

\\(w(l, r)\\) 满足四边形不等式,记 \\(k_r\\)\\(f_r\\) 的最小最优决策点,则(反证法可证明)

\\[\\forall r_1 \\leq r_2: \\quad k_{r_1} \\leq k_{r_2} \\]

使用分治法优化 DP(递归实现)可以给出 \\(O(n \\log n)\\) 的做法。

如果上述 \\(w(l, r) = f(r) - g(l)\\),并且 \\(f, g\\) 单调递增,则我们可以使用斜率优化 DP,\\(O(n)\\) 解决。本质是后面的决策单调性

四边形不等式函数类

  • \\(w_1(l, r), w_2(l, r)\\) 均满足四边形不等式(或区间包含单调性),则 \\(\\forall c_1, c_2 \\geq 0\\)\\(c_1 w_1 + c_2 w_2\\) 对应的也满足。
  • 若存在函数 \\(f, g\\) 使得 \\(w(l, r) = f(r) - g(l)\\) 则函数 \\(w\\) 满足四边形恒等式。若 \\(f, g\\) 单调递增,则 \\(w\\) 满足区间包含单调性。
  • \\(h(x), h\'(x)\\) 都是单调递增的,若 \\(w\\) 满足区间包含单调性和四边形不等式,则 \\(h(w)\\) 也满足。
  • \\(h\'(x)\\) 单调递增,若 \\(w\\) 满足区间包含单调性和四边形恒等式,则 \\(h(w)\\) 满足四边形不等式。

决策单调性分治优化 DP

例题:2017 ACM-ICPC Word Final D: Money for Nothing

\\(L_i = (x_i, y_i) x_i < x_{i + 1}, y_i > y_{i + 1}\\)
\\(R_i = (p_i, q_i) p_i < p_{i + 1}, q_i > q_{i + 1}\\)

\\(L_iR_j = (q_j - y_i)(p_j - x_i), \\quad (x_i < p_i, y_j < p_j)\\) 即围成的面积。
\\(u_i\\) 为 使得 \\(L_iR_j\\) 最大的 \\(j\\)(多个取下标最大的)。若 \\(i < j, k < l\\),画个图显然就有(有点四边形内味)

\\[L_iR_l + L_jR_k \\leq L_iR_k + L_jR_l \\]

因此 \\(u_i\\) 是单调递增的(因此这样就可以调整最优策略了,这也就给出了决策单调性)。

#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
using LL = long long;

int main() {
	// freopen("C:\\\\Users\\\\dna049\\\\cf\\\\in", "r", stdin);
	std::ios::sync_with_stdio(false);
	std::cin.tie(nullptr);
	int n, m;
	std::cin >> n >> m;
	std::vector<std::pair<int, int>> a(n), b(m);
	for (auto &[x, y] : a) std::cin >> x >> y;
	for (auto &[x, y] : b) std::cin >> x >> y;
	std::sort(a.begin(), a.end());
	int cnt = 1;
	for (int i = 1; i < n; ++i) {
		if (a[i].second < a[cnt - 1].second) a[cnt++] = a[i];
	}
	a.resize(cnt);
	std::sort(b.begin(), b.end());
	cnt = 1;
	for (int i = 1; i < m; ++i) {
		while (cnt > 0 && b[i].second >= b[cnt - 1].second) --cnt;
		b[cnt++] = b[i];
	}
	b.resize(cnt);
	// 确保了始终能找到最优策略
	b.insert(b.begin(), {0, INT_MAX});
	LL ans = 0;
	std::function<void(int, int, int, int)> divideConquer = [&](int l, int r, int opl, int opr) {
		// 区间都是左闭右开的
		if (l >= r) return;
		int mid = (l + r) / 2;
		LL rm = INT64_MIN;
		int k = -1;
		// 一开始必然满足 b[opl].second > a[mid].second,因此 k 必然满足
		for (int i = opl; i < opr && b[i].second > a[mid].second; ++i) {
			LL tmp = LL(b[i].second - a[mid].second) * (b[i].first - a[mid].first);
			if (tmp > rm) {
				rm = tmp;
				k = i;
			}
		}
		ans = std::max(ans, rm);
		divideConquer(l, mid, opl, k + 1);
		// b[k].second > a[mid].second > ... > a[r - 1].second,从而没有 k = -1 出现的可能
		divideConquer(mid + 1, r, k, opr);
	};
	divideConquer(0, a.size(), 0, b.size());
	std::cout << ans << "\\n";
	return 0;
}

如果这题不指定左下角节点和右上角节点。那么我们可以让所有节点成为左下角,所有节点成为右上角,就又变成此问题了。如果更进一步,不要求左下角和右上角,只要是斜对角线即可。那么我们可以让所有横坐标取相反数,再算一遍即可(妙啊)。

如果求最小值我目前还不知道有什么好方法。

仅有一个特殊元素分治优化

例题:1442D,详见官方题解

以上是关于动态规划的主要内容,如果未能解决你的问题,请参考以下文章

是否可以动态编译和执行 C# 代码片段?

动态规划_线性动态规划,区间动态规划

应对笔试手写代码,如何准备动态规划?

应对笔试手写代码,如何准备动态规划?

应对笔试手写代码,如何准备动态规划?

算法动态规划 ⑤ ( LeetCode 63.不同路径 II | 问题分析 | 动态规划算法设计 | 代码示例 )