最后更新于:2022年6月25日 下午
动态规划是研究一大类问题(特别是最值问题)的一种思路。从大二刚开始 ICPC 竞赛的时候第一次遇到,到大三学运筹学系统的了解,再到后来一直成为解决问题的一种思考方式。可以说动态规划真的是万金油的方法。
计算机领域(或者说博弈论)中的动态规划,就如同数学中的数学归纳法,一样重要。
运用动态规划的能力,就好比武侠小说中的内功,是随着时间慢慢累积的。 进阶可看 zscoder 的博文
动态规划应用举例
数列的递推关系
背包问题
图论中 Floyd 算法,Dijkstra 算法
流水线问题,旅行商问题
其它杂类问题
动态规划原理
动态规划(dynamic programming)于 1950s 被 Richard Bellman 发现。引用这里的话 ,动态规划本质就是:
定义问题的状态(必须满足"无后效性":这个就很玄学了)
写出状态间的转移方程
从而递推(分治)的解决问题。
难点就在于定义问题的状态
很多时候一个变量的问题,我们需要强行加一个(甚至两个)变量来定义状态
然后给出状态转移方程,再做时间空间的优化。
常规动态规划的状态定义
动态规划举例(长期更新)
有 \(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\) 号牌。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 #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 () { 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 ; }
这题大意:给你一些数,数值固定,部分数的位置随意调节,问你如何调解使得下式取值最大
\[
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])
详细转移见代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 #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 ; }
我们用 ans[i][j]
表示前 i
个数中以 j
结尾的最短数列长度,并用 b[i][j]
保存导致它以 j
结尾的前缀首项的位置。那么状态转移就显然,可从代码中读取。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 #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 () { 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 ; }
每一行单独考虑,然后因为数据范围,所以用 Python 交题
按照规模做 dp
,最近做的很少,记录一哈 2020/7/4
1 2 3 4 5 6 7 8 9 10 11 12 13 14 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)
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
进行空间优化,但是没必要。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 #include <bits/stdc++.h> using namespace std;using LL = long long ;int main () { 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]
\]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 #include <bits/stdc++.h> using namespace std;using LL = long long ;int main () { 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 ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 #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 () { 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 ())){ res+=x.second; } cout<<res%M<<endl; return 0 ; }
状态压缩 DP(mask dp)
给定 \(2n\) 个非负整数,求两两配对的最大公约数的和的最大值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 #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 () { std::ios::sync_with_stdio (false ); std::cin.tie (nullptr ); int n; std::cin >> n; n *= 2 ; std::vector<int > a (n) ; for (auto &x : a) std::cin >> x; 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; }; std::vector<LL> mask (1 << n) ; for (int step = 0 ; step < n; ++step) { 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 ; }
本问题可以在多项式时间用一般图匹配 解决。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 #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 () { 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 ; }
给定数列 \(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\) 没有交集。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 #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 () { 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]\)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 #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 () { 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)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 #include <bits/stdc++.h> #define watch(x) std::cout << (#x) << " is " << (x) << std::endl using LL = long long ;int main () { 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 ); 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。
其它例题:1324F 和 708C
斜率优化 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
必不可能成为最优点。因此相邻的节点斜率必然是递增的,因此合法队首对应的就是答案。用一个双向队列即可。(实现的时候变量有一些混用为了代码的简洁)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 #include <bits/stdc++.h> #define watch(x) std::cout << (#x) << " is " << (x) << std::endl using LL = long long ;int main () { 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]; } for (int i = 1 ; i <= n; ++i) { b[i] = b[i - 1 ] + x[i] * a[i]; a[i] += a[i - 1 ]; } 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)\) )
区间 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)\) 满足区间单调性和四边形恒等式,因此对于最小都可以用平行四边形优化。
对于最大值,用上述优化做法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 #include <bits/stdc++.h> #define watch(x) std::cout << (#x) << " is " << (x) << std::endl using LL = long long ;int main () { 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 给预处理出来。然后再转移。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 #include <bits/stdc++.h> #define watch(x) std::cout << (#x) << " is " << (x) << std::endl using LL = long long ;int main () { 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; 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\) 是单调递增的(因此这样就可以调整最优策略了,这也就给出了决策单调性)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 #include <bits/stdc++.h> #define watch(x) std::cout << (#x) << " is " << (x) << std::endl using LL = long long ;int main () { 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 ; 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 ); divideConquer (mid + 1 , r, k, opr); }; divideConquer (0 , a.size (), 0 , b.size ()); std::cout << ans << "\n" ; return 0 ; }
如果这题不指定左下角节点和右上角节点。那么我们可以让所有节点成为左下角,所有节点成为右上角,就又变成此问题了。如果更进一步,不要求左下角和右上角,只要是斜对角线即可。那么我们可以让所有横坐标取相反数,再算一遍即可(妙啊)。
如果求最小值我目前还不知道有什么好方法。
仅有一个特殊元素分治优化
例题:1442D ,详见官方题解