首页 > 其他分享 >决策单调性DP

决策单调性DP

时间:2024-10-09 16:43:59浏览次数:1  
标签:ll mid 决策 枚举 lt DP 单调

  • 决策单调性DP是一个非常重要的DP类别。在决策点随枚举点增加单调不降时,可以有效地优化复杂度。

  • 一般而言,决策点指的是对于一个 \(f[i]\),它的值需要从另一个值j中转移,而对于所有j,令 \(f[i]\) 最大的j值就是决策点。
    而其单调性体现在对于一个点i,它的决策点一定会大于等于i-1的决策点。如果此单调性成立,那么一般就会用二分缩小决策点值域或者一些单调的数据结构(一般为单调栈,有时也有单调队列)来减小复杂度。

  • 决策点的单调性大多数时候只能感性理解或者打表。在考场上证明最基本的需要四边形不等式。一旦证明卡住乃至错误,很有可能就会丢失大部分分数

  • 下面就是处理决策单调性的两种方法———二分与单调数据结构

二分

Yet Another Minimization Problem

  • 最朴素的DP就是设 \(f_{i,j}\) 代表对于前i个数,将其分为j段的最小代价
    转移方程也是呼之欲出, \(f_{i,j}=min_{k<i}(f_{k-1,j-1}+cacl(k,i))\),其中 \(cacl(k,i)\) 指k到i的贡献
  • 但就算可以 \(O(1)\) 求\(cacl\),时间复杂度也是 \(O(n^{2})\) 的。
    考虑单调性。由于对于 \(cacl(k,i)\) ,其值是由\(cacl(k,i-1)+\sum_{j=k}^{i}[a_{j}=a_{i}]\) 转移而来的,因此对于点 \(i\) ,对于它之前的两个点 \(u,v(u<v)\) ,如果 \(i\) 从 \(u\) 转移过来的值 \(f_{u,j-1}+cacl(u+1,i)\) 比从 \(v\) 转移过来的值 \(f_{v,j-1}+cacl(v+1,i)\) 大,那么对于点 \(i+1\) ,其 \(f_{i+1,j}\) 值如果从两者中转移,那对于 \(u\) 而言,\(f_{i+1,j}=f_{u,j-1}+cacl(u+1,i+1)\)。之前我们知道,\(cacl(a,b)\) 中随着 \(b\) 的增大,其值一定单调不降,所以从 \(v\) 转移有可能比从 \(u\) 转移更优,因为尽管 \(i\) 从 \(u\) 转移更优,但 对于\(i+1\) 加的 \(cacl\) 值比 \(v\) 大,因此皆有可能。但对于 \(u\) 之前一个点,如果\(i\) 从其转移比 \(u\) 劣,那 \(i+1\) 从其转移一定比 从 \(u\) 转移劣。因此,\(i+1\) 的决策点一定大于等于 \(i\) 的决策点。
  • 知道有单调性后,考虑到对于每个 \(f_{k,j}\) ,其决策点是独立的,只与上一层枚举的分为 \(j-1\) 段的 \(f_{1->k,j-1}\) 值有关,与同层之间的 \(f\) 值无关,因此直接考虑枚举将区间分为多少段,再二分每个 \(mid\),求出它的决策点后就知道了它前后点的决策点的范围,进一步缩小枚举范围。
void erfen(ll l,ll r,ll lt,ll rt)
{
	ll mid=(l+r)>>1,ml=max(1ll,lt),mr=min(mid,rt),pos;
	//这里注意mr的取值。我们要求的是mid的f值和其决策点的位置
	//但mid的决策点的位置起码不应小于自己 
	ll res=inf;
	for(ll i=ml;i<=mr;i++)
	{
		ll tmp=f[i-1][dep-1]+cacl(i,mid);
		if(tmp<res) res=tmp,pos=i;
	}
	f[mid][dep]=res;
	if(l==r) return;
	erfen(l,mid,lt,pos);
	erfen(mid+1,r,pos,rt);
}
  • 但如果要想做到 \(O(knlog_{2}n)\) 的复杂度,就还需要 \(O(1)\) 求 \(cacl\) 的值。考虑到对于上述代码中的每次二分的过程,在循环中枚举的左端点是连续上升的,而对于其枚举的左右区间,从 \(l,r\) 递归到 \(l,mid\),其询问的 \(cacl\) 的区间最多移动 \(r-l\) 次。所有区间垒起来会成为类似于线段树分割区间那样的形式。每递归一层 \(l,r\) 都会移动 \(n\) 次,而一共递归了 \(log_{2}n\) 层,因此对于每一个枚举的分为的段数, \(l,r\) 总共移动了 \(nlong_{2}n\) 次。因此考虑像莫队一样用桶和左右指针相对暴力地维护 \(cacl\) 的值。而二分的复杂度也是 \(O(nlog_{2}n)\) 的,因此总复杂度就为 \(O(knlong_{2}n)\)
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll inf=1e18;
const ll N=1e6+10;
ll n,k,dep=0,a[N],f[N][25],sum=0,cnt[N],L=1,R=0;
ll cacl(ll lt,ll rt)
{
	while(L>lt) sum+=cnt[a[--L]]++;
	while(R<rt) sum+=cnt[a[++R]]++;
	while(L<lt) sum-=--cnt[a[L++]]; 
	while(R>rt) sum-=--cnt[a[R--]];
	return sum;
}
void erfen(ll l,ll r,ll lt,ll rt)
{
	ll mid=(l+r)>>1,ml=max(1ll,lt),mr=min(mid,rt),pos;
	ll res=inf;
	for(ll i=ml;i<=mr;i++)
	{
		ll tmp=f[i-1][dep-1]+cacl(i,mid);
		if(tmp<res) res=tmp,pos=i;
	}
	f[mid][dep]=res;
	if(l==r) return;
	erfen(l,mid,lt,pos);
	erfen(mid+1,r,pos,rt);
}
int main()
{
	ios::sync_with_stdio(false);cin.tie(0),cout.tie(0);
	cin>>n>>k;
	for(int i=1;i<=n;i++) f[i][0]=inf;
	for(int i=1;i<=n;i++) cin>>a[i];
	while(dep<=k)
	{
		dep++;
		erfen(1,n,1,n);
	}
	cout<<f[n][k]<<endl;
	return 0;
}

单调数据结构

P1912 [NOI2009] 诗人小G

  • 先想出最朴素的DP状态 \(f_{i}\) 表示前 \(i\) 首诗所能凑出的最小代价。
  • 设 \(s_{i}=i+\sum_{j=1}^{i}len_{j}\),即前缀长度。加一是因为都要加空格。
    方程 \(f_{i}=min_{j<i}(f_{j}+|s_{i}-s_{j}-1|^{P})\),减一是因为还要减去行末空格
  • 显然暴力 \(O(n^{2})\),考虑如何优化。由于方程里有 \(P\) 次方项,不好数学方法转化或者数据结构维护,因此向单调性这方面靠。
  • 可惜我不会证四边形不等式不如打表。可以发现每个点的决策点所组成的序列可能长成如下的情况:
    112333334456
    这里的数字对应的是每个点从哪个点转移过来最优,我们可以发现两条相当显然的性质:
  1. 每个点的决策点都一定小于自己
  2. 每个点做出决策后决策点一定不动,其 \(f\) 值一定也是确定了的(其实就是无后效性)
  • 那我们就想到假设某个点的最优决策点已经找到,那可不可以在枚举到它的时候将其后面的点的最优决策点是它的点也一起处理了呢?
  • 但这样的想法有一个问题,由前面的点更新后面的点很有可能后面的点又再次被更新,比如枚举到3的时候:
    122|3333333
    但枚举到4的时候:
    1223|344444
  • 这样我们就需要维护一个支持修改的单调的序列。同时,上面的过程中,竖线前的数我们已经处理了,需要被去掉。那么这个数据结构已经近在眼前了——单调队列。
  • 这个单调队列不可能真的存下一整个真的完整序列,也没必要。我们考虑去存每一段连续的相同的区间的左右端点。设处理到一个点 \(i\),其决策点为 \(q_{head}\),就将决策点右端点小于 \(i\) 的区间删掉,然后在其右边的区间里二分从哪个点开始转移更优。比如说对于上面枚举到4的这个例子,我们要找的就是其左边这个点的决策点不是4而其本身决策点是4的加粗的这个点
    1223|344444
    由于这个点右边的所有点从4转移都比3更优,因此就可以二分。
  • 不过二分有几个需要注意的点:
  1. 可能有某些决策点的整个区间都被覆盖完
    如:
    12233|33345
    122333|6666
    这时,就需要先判断单调队列末尾的区间的左端点从原来决策点转移与从现在这个点转移的优劣。
    如果从原来转移优,那就在这个区间里二分(之前说的断点一定在这个区间里)
    否则,这个区间一定会被现在枚举的这个点完全覆盖,因此直接删除即可(因为除非极劣,否则新加入的这个区间右端点一定是n)
  2. 可能这个决策点很劣,完全覆盖不了
    这种情况就在做上一步之前特判一下最后一个点从它原来的决策点转移好还是从当前枚举的点转移好就行了。
  • 输出并不是主要要讲的,但还是有些坑点,这里提一下。
    1.要用long double,因为中间并不是最优的决策点的答案超过 \(1e18\),因此需要用 long double舍弃一些精度来提高存储量(long double会自动帮你用科学计数法舍弃一些精度来存储数据)
    2.在二分的时候需要对每个区间存储一下每个点对应的决策点位置,称为 \(lst_{i}\),再用其更新 \(nxt_{i}\) 来表示在哪一个数的位置换行,依次输出,注意 \(nxt_{i}\) 是反着更新的
  • 时间复杂度的话,对于枚举的每个点,其最多入队、出队一次,对于枚举的每个点最差情况下都要二分,因此会算 \(nlong_{2}n\) 次 \(|s_{i}-s_{j}-1|^{P}\),而这个东西还需要快速幂 \(log_{2}n\) 去算,因此总复杂度为\(O(Tnlog_{2}^{2}n)\)
#include<bits/stdc++.h>
using namespace std;
typedef long long ld;
typedef long long ll;
//#define int ll
const ld inf=1e18*1.0;
const ll N=2e5+10;
ll n,L,P,len[N],sum[N],tail,head,l[N],r[N],q[N];
ll lst[N],nxt[N];
ld f[N];

string s[N];
ld ksm(ld x)
{
	ld res=1;ll k=P;
	while(k)
	{
		if(k&1) res=res*x;
		x*=x,k>>=1;
	}
	return res;
}
#define getf(x,y) ((ld)((ld)f[y]+ksm((ld)abs(sum[x]-sum[y]-L-1))))
void shuru()
{
	cin>>n>>L>>P;
	for(ll i=1;i<=n;i++)
	{
		cin>>s[i];
		len[i]=(ll)s[i].length()+1;
		sum[i]=sum[i-1]+len[i];
	}
}

void erfen(ll x)
{
	ll now=q[tail],lt=l[now],rr=n;
	while(lt<rr)
	{
		ll mid=(lt+rr)>>1;
		if(getf(mid,now)>getf(mid,x)) rr=mid;
		else lt=mid+1;
	}
	r[now]=lt-1,q[++tail]=x;l[x]=lt,r[x]=n;//更新左右区间范围 
}

void cacl()
{
	head=tail=0;
	q[0]=0,l[0]=1,r[0]=n;
	for(ll i=1;i<=n;i++)
	{
		while(r[q[head]]<i) head++;
		ll now=q[head];
		f[i]=getf(i,now);lst[i]=now;
		if(getf(n,q[tail])<getf(n,i)) continue;
		while(getf(l[q[tail]],q[tail])>getf(l[q[tail]],i)) tail--;
		erfen(i);
	}
}
int T;
void write()
{
	if(f[n]>inf) cout<<"Too hard to arrange"<<'\n';
	else
	{
		cout<<(ll)(f[n]+0.5)<<'\n';
		for(ll i=n;i;i=lst[i]) nxt[lst[i]]=i;//注意i是倒着跳lst的 
		ll now=0;
		for(ll i=1;i<=n;i++)
		{
			now=nxt[now];
			for(ll j=i;j<now;j++) cout<<s[j]<<' ';
			cout<<s[now]<<'\n';
			i=now;  //i不用再加1了,for会帮你加 
		}
	}
//	puts("--------------------");
	if(!T)
	cout<<"--------------------";
	else cout<<"--------------------"<<'\n';
	
}

signed main()
{
	ios::sync_with_stdio(false);cin.tie(0),cout.tie(0);
	cin>>T;
	while(T--)
	{
		shuru();
		cacl();
		write();
	}
}

标签:ll,mid,决策,枚举,lt,DP,单调
From: https://www.cnblogs.com/allforgod/p/18446029

相关文章

  • (LeetCode 热题 100) 1143. 最长公共子序列(动态规划dp)
    题目:1143.最长公共子序列思路:经典动态规划dp题型,时间复杂度为0(n^2)。C++版本:classSolution{public:intlongestCommonSubsequence(stringtext1,stringtext2){intn=text1.size(),m=text2.size();//状态f[i][j]表示:text1[0,i]和text2[0......
  • 计算机网络 tcp和udp
    目录一、TCP建立连接-TCP三次握手1)什么是半连接队列和全连接队列?2)为什么要三次握手?3)三次握手过程中可以携带数据吗?断开连接-TCP四次挥手1)为什么要四次挥手?2)为什么不能把服务端发送的ACK和FIN合并起来,变成三次挥手?3)如果第二次挥手时服务端的ACK没有送......
  • wordpress建立数据库连接时出错怎么办
    WordPress在建立数据库连接时出现问题通常是因为配置文件 wp-config.php 中的信息不正确或数据库本身的问题。你可以按照以下步骤来排查和解决问题:检查配置文件:打开你的WordPress安装目录下的 wp-config.php 文件。确认数据库名称(DB_NAME)、用户名(DB_USER)、密码......
  • dp01
    摘花生题目描述她来到一片有网格状道路的矩形花生地(如下图),从西北角进去,东南角出来。地里每个道路的交叉点上都有种着一株花生苗,上面有若干颗花生,经过一株花生苗就能摘走该它上面所有的花生。H只能向东或向南走,不能向西或向北走。问H最多能够摘到多少颗花生。这是AcWing上的......
  • Hetao P1031 萌萌题 题解 [ 蓝 ] [ 线性 dp ]
    萌萌题:一道结合了观察性质的线性dp。观察我们先考虑极端情况:所有数相同,所有数降序排列两种情况。对于所有数相同的情况,我们发现,最终可以合并出来的区间,最多只有\(n\logn\)个。怎么证明?考虑固定右端点,那么我们想要合并出一个点,就得选\(2^k\)个数出来,这就有\(\logn\)......
  • 【模板】"动态DP"&动态树分治
    目录题目描述朴素算法矩阵刻画实现code以洛谷模板题为例介绍动态dp的一般方法。P4719【模板】"动态DP"&动态树分治-洛谷|计算机科学教育新生态(luogu.com.cn)P4751【模板】"动态DP"&动态树分治(加强版)-洛谷|计算机科学教育新生态(luogu.com.cn)题目描述给定一......
  • 单调栈/单调队列
    1.单调栈给定一个长度为\(n\)的数列\(a\),对每个数字求出其右/左边第一个值大于等于它的数字的位置。考虑从左到右扫整个序列,维护一个栈,里面存放可能成为答案的数字,当遍历到一个新的数\(a_i\)的时候,可以发现栈中\(\leqa_i\)的数就再也不可能成为答案了,那就把它们弹掉,......
  • 前端的全栈混合之路Meteor篇:分布式数据协议DDP深度剖析
    本文属于进阶篇,并不是太适合新人阅读,但纯粹的学习还是可以的,因为后续会实现很多个ddp的版本用于web端、nodejs端、安卓端和ios端,提前预习和复习下。ddp协议是一个C/S架构的协议,但是客户端也同时可以是服务端。什么是DDP?DDP(DistributedDataProtocol)是Meteor框架中......
  • WordPress 6.7即将发布的新功能(和截图)
    我们一直在密切关注WordPress6.7的开发并测试该版本的测试版,它将带来一些令人兴奋的更新和几个新功能。例如,我们很高兴地发现即将发布的版本将附带全新的默认主题,并对块编辑器和站点编辑体验进行大规模改进。在本文中,我们将向您介绍WordPress6.7中的主要功能。每项功能......
  • CF708C Centroids [树形DP,换根DP]
    Description给定一棵树。至多进行一次操作:删去一条边,连接一条新边,保证操作完后仍是树。问每个点在进行操作后是否可以成为树的重心。Solution性质\(1\):若一个点不是树的重心,则它的必然有一个大小大于\(\lfloorn/2\rfloor\)的子树。性质\(2\):如果一个点合法,要么它本来......