线段树优化 dp 系列 题解

1. 洛谷 P4644 USACO05DEC Cleaning Shift/AT_dp_w Intervals

之前写过,加入这个系列。

(贪心、线段树优化)洛谷 P4644 USACO05DEC Cleaning Shift/AT_dp_w Intervals 题解

2. 洛谷 P2344 USACO11FEB Generic Cow Protests

题意

Farmer John 的 N N N 头奶牛( 1 ≤ N ≤ 1 0 5 1 \leq N \leq 10^5 1N105)排成一列,正在进行一场抗议活动。第 i i i 头奶牛的理智度为 a i a_i ai − 1 0 4 ≤ a i ≤ 1 0 4 -10^4 \leq a_i \leq 10^4 104ai104)。

FJ 希望奶牛在抗议时保持理性,为此,他打算将所有的奶牛隔离成若干个小组,每个小组内的奶牛的理智度总和都要不小于零。

由于奶牛是按直线排列的,所以一个小组内的奶牛位置必须是连续的。请帮助 FJ 计算一下,满足条件的分组方案有多少种。

输出满足条件的分组方案对 1 0 9 + 9 10^9+9 109+9 取模的结果。

思路

先考虑朴素的 dp,设 f i f_i fi 表示,对 1 ∼ i 1\sim i 1i 分好组后, i i i 处于最新一组的最后一个,总的方案数。那么有:
f i = ∑ j < i f j , ( ∑ k = j + 1 i a k ≥ 0 ) f_i=\sum_{j<i}f_j,\left(\sum_{k=j+1}^ia_k\ge0\right) fi=j<ifj, k=j+1iak0

不妨预处理一个前缀和, s i = ∑ j = 1 i a j s_i=\sum_{j=1}^ia_j si=j=1iaj,那么转移条件就是 s i ≥ s j s_i\ge s_j sisj

可是这不就要 Θ ( n 2 ) \Theta(n^2) Θ(n2) 了吗?考虑优化,我们发现,只有 s i ≥ s j s_i\ge s_j sisj 时才能转移,那么每次转移的时候,查询状态 j ∈ [ 1 , i ) j\in[1,i) j[1,i) 中前缀和 s j ∈ [ min ⁡ { s } , s i ] s_j\in[\min\{s\},s_i] sj[min{s},si] 的合法状态 j j j,所对应的 f j f_j fj 总和。

这其实相当于维护区间 [ min ⁡ { s } , s i ] [\min\{s\},s_i] [min{s},si] 的状态和,可以扔上线段树维护。
f i = query ( min ⁡ { s } , s i ) f_i=\textrm{query}(\min\{s\},s_i) fi=query(min{s},si)

然后更新和为 s i s_i si 处的状态:
modify ( s i , f i ) \textrm{modify}(s_i,f_i) modify(si,fi)

答案就是 f n f_n fn 了。

不过鉴于前缀和值域较大,考虑离散化。

记得对 1 0 9 + 9 10^9+9 109+9 取模。

#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define ls u<<1
#define rs u<<1|1
const ll N=1e5+9,mod=1e9+9;
ll n,a[N],nn;
ll s[N],ss[N],b[N];
struct SegT
{
	struct node
	{
		ll sum;
	}T[N<<2];
	void pushup(ll u)
	{
		T[u].sum=(T[ls].sum+T[rs].sum)%mod;
	}
	void modify(ll u,ll l,ll r,ll x,ll k)
	{
		if(l==r)
		{
			T[u].sum=(T[u].sum+k)%mod;
			return;
		}
		ll mid=(l+r)>>1;
		if(x<=mid)modify(ls,l,mid,x,k);
		if(x>mid)modify(rs,mid+1,r,x,k);
		pushup(u);
	}
	ll query(ll u,ll l,ll r,ll ql,ll qr)
	{
		if(qr<l||r<ql)return 0;
		if(ql<=l&&r<=qr)return T[u].sum;
		ll mid=(l+r)>>1,ret=0;
		if(ql<=mid)ret=(ret+query(ls,l,mid,ql,qr))%mod;
		if(qr>mid)ret=(ret+query(rs,mid+1,r,ql,qr))%mod;
		return ret;
	}
}A;
int main()
{
	scanf("%lld",&n);
	a[1]=0;
	n++;
	for(int i=2;i<=n;i++)
	scanf("%lld",&a[i]);
	for(int i=1;i<=n;i++)
	ss[i]=s[i]=s[i-1]+a[i];
	sort(ss+1,ss+n+1);
	nn=unique(ss+1,ss+n+1)-ss-1;
	for(int i=1;i<=n;i++)
	b[i]=lower_bound(ss+1,ss+nn+1,s[i])-ss;
	A.modify(1,1,n,b[1],1);
	ll ret=0;
	for(int i=2;i<=n;i++)
	{
		ret=A.query(1,1,n,1,b[i]);
		A.modify(1,1,n,b[i],ret);
	}
	printf("%lld",ret);
	return 0;
}

3. CF474E Pillars

题意

n n n 根柱子从左往右排成一行,第 i i i 根柱子的高度是 a i a_i ai

一只猴子一开始在第 1 1 1 根柱子,他每次可以向右边跳,但是有限制:若猴子当前在第 j j j 根柱子,可以跳到第 i i i 根柱子,仅当 i > j , ∣ a i − a j ∣ ≥ d i>j,|a_i-a_j|\ge d i>j,aiajd

问猴子最多可以跳到多少根柱子?并输出其中一种可行的跳跃方案。

1 ≤ n ≤ 1 0 5 , 0 ≤ d ≤ 1 0 9 , 1 ≤ a i ≤ 1 0 15 1\le n\le 10^5,0\le d\le 10^9,1\le a_i\le 10^{15} 1n105,0d109,1ai1015

思路

f i f_i fi 表示跳到了 i i i,最多的柱子数。那么有:
f i = max ⁡ j < i { f j } + 1 , ( ∣ a i − a j ∣ ≥ d ) f_i=\max_{j<i}\{f_j\}+1,(|a_i-a_j|\ge d) fi=j<imax{fj}+1,(aiajd)

显然超时,考虑优化。其实和上一题类似,就是在一堆符合条件的状态里面,挑一个最大的。

让我们看到条件: ∣ a i − a j ∣ ≥ d |a_i-a_j|\ge d aiajd,则若 a j ∈ [ min ⁡ { a } , a i − d ] ∪ [ a i + d , max ⁡ { a } ] a_j\in[\min\{a\},a_i-d]\cup [a_i+d,\max\{a\}] aj[min{a},aid][ai+d,max{a}],那么 j j j 就是一个合法的状态,我们发现 j j j 就在两段区间里面,就是区间查询最大值了,扔上线段树,查询对应区间的状态 f j f_j fj 最大值。
f i = max ⁡ ( query ( min ⁡ { a } , a i − d ) , query ( a i + d , max ⁡ { a } ) ) f_i=\max(\textrm{query}(\min\{a\},a_i-d),\textrm{query}(a_i+d,\max\{a\})) fi=max(query(min{a},aid),query(ai+d,max{a}))

发现 a i a_i ai d d d 值域较大,记得离散化。

至于方案,记录转移点然后 dfs 倒序输出即可。

代码

#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define ls u<<1
#define rs u<<1|1
const ll N=3e5+9,inf=0x7f7f7f7f;
ll n,d,a[N];
ll aa[N],nn;
ll f[N],fat[N];
ll tot,oup[N];
void print(ll x)
{
	if(!x)return;
	oup[++tot]=x;
	print(fat[x]);
}
struct node
{
	ll ma,id;
};
node nmx(node x,node y)
{
	if(x.ma>y.ma)return x;
	return y;
}
struct SegT
{
	node T[N<<2];
	void pushup(ll u)
	{
		T[u]=nmx(T[ls],T[rs]);
	}
	void modify(ll u,ll l,ll r,ll x,ll k,ll id)
	{
		if(l==r)
		{
			if(k>T[u].ma)
			{
				T[u].ma=k;
				T[u].id=id;
			}
			return;
		}
		ll mid=(l+r)>>1;
		if(x<=mid)modify(ls,l,mid,x,k,id);
		if(x>mid)modify(rs,mid+1,r,x,k,id);
		pushup(u);
	}
	node query(ll u,ll l,ll r,ll ql,ll qr)
	{
		if(ql<=l&&r<=qr)return T[u];
		ll mid=(l+r)>>1;
		node ret;
		ret.ma=-inf;
		if(ql<=mid)ret=nmx(ret,query(ls,l,mid,ql,qr));
		if(qr>mid)ret=nmx(ret,query(rs,mid+1,r,ql,qr));
		return ret; 
	}
}A;
int main()
{
	scanf("%lld%lld",&n,&d);
	for(int i=1;i<=n;i++)
	{
		scanf("%lld",&a[i]);
		aa[i]=a[i];
		aa[i+n]=a[i+n]=a[i]-d;
		aa[i+2*n]=a[i+2*n]=a[i]+d;
	}
	sort(aa+1,aa+3*n+1);
	ll nn=unique(aa+1,aa+3*n+1)-aa-1;
	for(int i=1;i<=3*n;i++)
	a[i]=lower_bound(aa+1,aa+nn+1,a[i])-aa;
	ll ans=0,id;
	f[1]=1;
	A.modify(1,1,nn,a[1],f[1],1);
	for(int i=2;i<=n;i++)
	{
	/*	ll x=lower_bound(aa+1,aa+nn+1,a[i])-aa;
		ll L=lower_bound(aa+1,aa+nn+1,a[i]-d)-aa;
		ll R=lower_bound(aa+1,aa+nn+1,a[i]+d)-aa;*/
		ll x=a[i],L=a[i+n],R=a[i+2*n];
		node s=A.query(1,1,nn,1,L),t=A.query(1,1,nn,R,nn);
		node c=nmx(s,t);
		f[i]=c.ma+1;
		fat[i]=c.id;
		//cout<<i<<" "<<c.id<<"\n";
		A.modify(1,1,nn,a[i],f[i],i);
		if(f[i]>ans)
		{
			ans=f[i];
			id=i;
		}
	}
//	cout<<id<<endl;
	printf("%lld\n",ans);
	print(id);
	for(int i=tot;i>=1;i--)
	cout<<oup[i]<<" ";
	return 0;
}

4. 洛谷 P1848 USACO12OPEN Bookshelf

题意

在这里插入图片描述

思路

f i f_i fi 表示以 i i i 为末尾,对 j ∼ i j\sim i ji 新开一层书架的最小答案。那么有:
f i = min ⁡ j = 1 i { f j − 1 + max ⁡ k = j i h k } ( ∑ k = j i w i ≤ L ) f_i=\min_{j=1}^i\left\{f_{j-1}+\max_{k=j}^ih_k\right\}\left(\sum_{k=j}^iw_i\le L\right) fi=j=1mini{fj1+k=jmaxihk} k=jiwiL

对于限制条件,由于前缀和单调,我们可以用双指针 Θ ( n ) \Theta(n) Θ(n) 地预处理出满足条件的 j j j 的下界。

显然是 Θ ( n 2 ) \Theta(n^2) Θ(n2) 的。

但是转移式子里面又有 min ⁡ \min min 又有 max ⁡ \max max 着实难搞。

f j f_j fj 显然是线段树区间查询最小值,如果我们找到一个 f j f_j fj 之后先不管,直接叠上 h i h_i hi 扔上线段树,那就考虑在后面的遍历中修改那一坨 max ⁡ \max max

我们可以用单调栈维护 max ⁡ k = j + 1 i h k \max_{k=j+1}^ih_k maxk=j+1ihk,如果栈顶被弹出,那就在线段树上直接修改即可。

我的写法是,在 1 ∼ i 1\sim i 1i 找合法的状态,向 i + 1 i+1 i+1 转移,叠加的是 h i + 1 h_{i+1} hi+1

代码

#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define ls u<<1
#define rs u<<1|1
const ll N=1e5+9,inf=3e14;
ll n,L,h[N],w[N];
ll las[N];
stack<ll>stk;
struct SegT
{
	struct node
	{
		ll mi;
		ll tag;
	}T[N<<2];
	void pushup(ll u)
	{
		T[u].mi=min(T[ls].mi,T[rs].mi);
	}
	void pushdown(ll u)
	{
		if(T[u].tag)
		{
			T[ls].mi+=T[u].tag;
			T[ls].tag+=T[u].tag;
			T[rs].mi+=T[u].tag;
			T[rs].tag+=T[u].tag;
			T[u].tag=0; 
		}
	}
	void modify(ll u,ll l,ll r,ll ql,ll qr,ll k)
	{
		if(ql<=l&&r<=qr)
		{
			T[u].mi+=k;
			T[u].tag+=k; 
			return;
		}
		pushdown(u); 
		ll mid=(l+r)>>1;
		if(ql<=mid)modify(ls,l,mid,ql,qr,k);
		if(qr>mid)modify(rs,mid+1,r,ql,qr,k);
		pushup(u);
	}
	ll query(ll u,ll l,ll r,ll ql,ll qr)
	{
		if(ql<=l&&r<=qr)return T[u].mi;
		pushdown(u);
		ll mid=(l+r)>>1,ret=inf;
		if(ql<=mid)ret=min(ret,query(ls,l,mid,ql,qr));
		if(qr>mid)ret=min(ret,query(rs,mid+1,r,ql,qr));
		return ret;
	} 
}A;
int main()
{
//	freopen("P1848_4.in","r",stdin);
	scanf("%lld%lld",&n,&L);
	for(int i=1;i<=n;i++)
	scanf("%lld%lld",&h[i],&w[i]);
	ll mas=0,j=0;
	for(int i=1;i<=n;i++)
	{
		mas+=w[i];
		while(mas>L)
		{
			j++;
			mas-=w[j];
		}
		las[i]=j+1;
	}
/*	for(int i=1;i<=n;i++)
	cout<<las[i]<<"~"<<i<<endl;*/
	A.modify(1,1,n+1,1,1,h[1]);
	h[0]=inf;
	stk.push(0);
	ll ret;
	for(int i=1;i<=n;i++)
	{
	//	cout<<i<<":";
		while(!stk.empty()&&h[i]>h[stk.top()])
		{
			ll x=stk.top();
			stk.pop();
		//	cout<<stk.top()<<"~"<<x<<" "<<h[i]<<"<="<<h[x]<<"\n";
			A.modify(1,1,n+1,stk.top()+1,x,h[i]-h[x]);
		}
	//	cout<<"\n"<<i<<"的top:"<<stk.top()<<endl;
		stk.push(i);
		ret=A.query(1,1,n+1,las[i],i);
		A.modify(1,1,n+1,i+1,i+1,ret+h[i+1]);
	}
	printf("%lld",ret);
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值