20241222北京总结

今天讲了 \(dp\) , 感觉讲的还挺基础的 , 但我也并没有全听懂 ( 悲 )

\(dp\) 东西有点多 , 不变的核心就那几条 , 我们直接上小智说的话 :

image

\(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\)

不会 , 暂搁置

posted @ 2024-12-22 21:03  2019yyy  阅读(24)  评论(0)    收藏  举报