20241222北京总结
今天讲了 \(dp\) , 感觉讲的还挺基础的 , 但我也并没有全听懂 ( 悲 )
\(dp\) 东西有点多 , 不变的核心就那几条 , 我们直接上小智说的话 :

\(so\)还是主要着眼于状态表示 , 牢记这一点 , 一个好的状态是优化的前提
这篇不着重于题 , 以下有很多题我也没写 , 就是自己口胡的 , 主要来看看算法内容
背包
\(01\) 背包 , 完全背包 , 多重背包极其优化还真就是挺板的 , 直接看题
P4141消失之物
考虑到它是方案数背包 , 直接回退或者说把之前的转移减掉都行
\(btw\) , 这题好像有背包合并 \(FFT\) 优化卷积的神秘做法 , 但我不会多项式 \(/kk\)
/*
* @Author: 2019yyy
* @Date: 2024-12-22 08:13:43
* @LastEditors: 2019yyy
* @LastEditTime: 2024-12-22 08:39:55
* @FilePath: \code\20241222\P4141.cpp
* @Description:
*
* I love Chtholly forever
*/
#include<bits/stdc++.h>
using namespace std;
int w[11000000];
int f[2100],g[2100];
int main(){
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int n,m;
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>w[i];
}
f[0]=1;
for(int i=1;i<=n;i++){
for(int j=m;j>=w[i];j--){
f[j]+=f[j-w[i]];
f[j]%=10;
}
}
for(int i=1;i<=n;i++){
memcpy(g,f,sizeof(f));
for(int j=w[i];j<=m;j++){
g[j]-=(g[j-w[i]]-10);
g[j]%=10;
}
for(int j=1;j<=m;j++){
cout<<g[j];
}
cout<<'\n';
}
return 0;
}
P8392 [BalticOI 2022 Day1] Uplifting Excursion
\(l\) 太大了 , 考虑缩小, 于是贪心的全选 , 完了不断往回还 , 直到 \(l\) 在 \([-n,n]\) 的范围内 , 做一个 \(n^2\) 范围的背包
为什么是 \(n^2\) 呢 , \(x\) 与 \(-x\) 不在背包中同时出现
P4322 [JSOI2016] 最佳团体
看到形式就发现它是一个 \(0/1\) 分数规划 , 一个显然的思想是二分 .
怎么验证呢 , 考虑以 \(p-mid*s\) 为值做树上背包 , 选 \(k\) 个能选出非负就行
#include<bits/stdc++.h>
using namespace std;
double s[1100000],p[1100000];
struct Edge{
int next,to;
} a[1100000];
int cnt,head[1100000];
int siz[1100000];
void addEdge(int x,int y){
a[++cnt].next=head[x];
a[cnt].to=y;
head[x]=cnt;
}
double val[1100000],f[2510][2510];
double eps=1e-5;
int k,n;
void dfs(int x,int fa){
siz[x]=1;
f[x][1]=val[x];
for(int i=head[x];i;i=a[i].next){
int to=a[i].to;
if(to==fa){
continue;
}
dfs(to,x);
siz[x]+=siz[to];
for(int j=min(siz[x],k+1);j>=1;j--){
for(int k=0;k<=(siz[to],j-1);k++){
f[x][j]=max(f[x][j],f[x][j-k]+f[to][k]);
}
}
}
}
bool check(double mid){
for(int i=1;i<=n;i++){
val[i]=p[i]-mid*s[i];
}
memset(f,-0x3f,sizeof(f));
dfs(0,0);
if(f[0][k+1]>=0){
return true;
}
return false;
}
int main(){
cin>>k>>n;
for(int i=1;i<=n;i++){
int x;
cin>>s[i]>>p[i]>>x;
addEdge(x,i);
}
double l=0,r=100000;
while(l+eps<r){
double mid=(l+r)/2;
if(check(mid)){
l=mid;
}else{
r=mid;
}
}
cout<<setprecision(3)<<fixed<<l<<'\n';
return 0;
}
区间 \(dp\)
感觉对区间 \(dp\) 有一定新理解 , 区间 \(dp\) 重在找转移点 ( 决策点 ) , 然后类似分治的化为子问题 , 之后合并 .
时间复杂度主要耗费在枚举转移点 , 所以能快速找到转移点就是这类问题优化的关键 .
一般找转移点 , 我们要看看单调性
P6563 [SBCOI2020] 一直在你身旁
容易看出是区间 \(dp\) , 然后就盯单调性 \(f_{i,k}\) 随 \(k\) 增大增大 , \(f_{k,j}\) 随 \(k\) 增大减小
我们观察转移式子 , 一个增一个减 , 分开讨论
前者单调 , 无脑取最左端 ; 后者上单调队列
接下来的问题就是这个交点在什么地方 , 发现 \(i,j\) 增的时候交点不降 , 所以直接转移看是否加一就行
\(btw\) , 我很喜欢这道题 , 不论是题面还是做法
#include<bits/stdc++.h>
using namespace std;
long long a[7100];
long long f[7100][7100];
int g[7100];
deque<int> q[7100];
int main(){
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int T;
cin>>T;
while(T--){
int n;
cin>>n;
for(int i=1;i<=n;i++){
cin>>a[i];
while(not q[i].empty()){
q[i].pop_back();
}
q[i].push_back(i);
}
for(int len=0;len<=n-1;len++){
for(int i=1,j=i+len;j<=n;i++,j++){
if(len==0){
f[i][j]=0;
g[j]=i;
continue;
}
if(len==1){
f[i][j]=a[i];
g[j]=j;
continue;
}
int t=g[j];
while(t>i and f[i][t-1]>f[t][j]){
t--;
}
f[i][j]=f[i][t]+a[t];
while((not q[j].empty()) and q[j].front()>=t){
q[j].pop_front();
}
if(not q[j].empty()){
f[i][j]=min(f[i][j],f[q[j].front()+1][j]+a[q[j].front()]);
}
g[j]=t;
while((not q[j].empty()) and f[q[j].back()+1][j]+a[q[j].back()]>=f[i+1][j]+a[i]){
q[j].pop_back();
}
q[j].push_back(i);
}
}
cout<<f[1][n]<<'\n';
}
return 0;
}
四边形不等式
主要应对一类快速求 \(f_i=\min_{1\leq j\leq i}w(j,i)\) 的问题 , 如果 \(w\) 满足某一类性质 ( 即四边形不等式 ) 我们就能优化
什么是四边形不等式 : 交叉优于包含
如果四边形不等式成立了 , 我们上文提到的问题就具有决策单调性了 , 如果计 \(f_i\) 决策点是 \(opt_i\) , 那么 \(opt_i\) 单调不降
有了决策单调性 , 下面是两种把算法优化成线性对数复杂度的方法
分治
一般用于多阶段的决策单调性
先求 \(opt_{n/2}\) , 然后分开看左右两边的 \(opt\) , 左边的 \(opt\) 不大于右边 , 分治时记录上下边界
贺一下 \(oiwiki\) 的伪代码 , 注释是我自己加的
int w(int j, int i); //待求函数
void DP(int l, int r, int k_l, int k_r) {//[l,r]待求区间 [k_l,k_r]决策点范围
int mid = (l + r) / 2, k = k_l;//求状态f[mid]的最优决策点
for (int j = k_l; j <= min(k_r, mid - 1); ++j)
if (w(j, mid) < w(k, mid)) k = j;//找决策点
f[mid] = w(k, mid);//根据决策单调性得出左右两部分的决策区间,递归处理
if (l < mid) DP(l, mid - 1, k_l, k);//左面小于等于k
if (r > mid) DP(mid + 1, r, k, k_r);//右面大于等于k
}
二分队列
常用
每个决策点能处理的问题都是一个区间 , 用单调队列记录每个决策点能处理哪些问题 , 在上面二分
把所有转移范围不包括后面要处理元素的决策点弹出 , 队尾中决策点最左的转移若不优于同位置下当前决策点的转移 , 那它以后也永远不可能优 , 直接弹出
接下来考虑当前决策点的决策范围 , 二分处理比队首优的第一个位置
斜率优化
把转移方程变成一次函数的斜截式 \(y=kx+b\) , 我们将与转移点 \(j\) 有关的信息表示为 \(y\) 的形式 , 把同时与 \(i,j\) 有关的信息表示为 \(kx\) , 把与 \(i\) 有关的信息表示为 \(b\)
这时候 , 把 \(( x , y )\) 看作平面内的点 , 我们就要最小化截距
图画出来我们发现 , 所有可能成为决策点的点都在一个凸包上
上面不难找决策点 , 因为它的斜率单调 , 但是有的不单调 , 所以这时候我们要上二分或 \(cdq\) 分治 , 但是我现在还不会 , 之后再补
\(wqs\) 二分
膜拜 \(wqs\) 学长 , 但是我现在还不会 ( \(/kk\) ) ,之后再补
\(slope\) \(trick\)
不会 , 暂搁置

浙公网安备 33010602011771号