VJ-Cleaning Shifts-(线段树优化dp+优化dp的整理)

Cleaning Shifts

题意:
就是给你n个奶牛,和m天。每个奶牛会在l[i]到r[i]天打扫卫生。现在问你最少要选多少个奶牛,可以使得从第1天到第m天,每天至少有一个奶牛打扫。

思考:

  1. 很经典的区间覆盖问题,就是给你n个线段,选择最少的线段覆盖整个区间。既然最小的覆盖,那么可以定义dp[i]为从1到i点都被覆盖的最小花费。那么就可以枚举线段了,线段的顺序怎么枚举呢?我们可以按照右端点从小到大排序,因为既然定义的dp[i]为走到i,那么按照右端点的时候,枚举到这个r,就可以从已经到达[l,r]这段区间的转移过来。如果按照左端点排序,不太好做,反正排序就这两种。
  2. 确定好枚举的顺序和dp的时候,直接跑一遍就可以了。枚举到[l,r]时候,dp[r]就可以从dp[l-1,r]这段区间来转移,就求这段区间的最小花费即可。值得注意的是初始化的时候,线段树要为inf,并且线段树建树的时候区间搞清楚,是从0到m,还是从1到m,这个范围不能错。线段树更新的时候,就更新dp[r]这个点就可以了。
  3. 对于dp转移的一些整理:
    1.如果dp的值是单调的,并且dp从前面转移的时候,可以从一个前缀来转移,那么可以直接维护一个全局idx。2022蓝桥杯国赛B组-费用报销2020沈阳-The Boomsday Project
    2.如果dp的值不单调,但是转移的时候是从前面的某一段区间来转移,并且这个区间是一直往右边一点一点移动的,那么可以用单调队列。2022江苏省赛-Jump and Treasure
    3.如果dp的值不单调,转移的时候是从前面的一段区间来转移,但是这个区间并不确定,那么就可以用线段树来优化。VJ-Cleaning Shifts 洛谷-Cleaning Shifts S。(洛谷这道题这本题一样,只是变成了求一段区间[st,ed]的最小花费。)
    4.如果dp值就是从前面某些点转移,但是这些点是离散的,不好直接加过来,并且对于每个点他可以更新后面的某段区间,那么可以用刷表法,换一下枚举顺序然后用每个点去更新后面的一段区间。这样时间和空间复杂度O(n3)的一般就变成O(n2)的了。牛客多校-Two Frogs

代码:

#include<iostream>
#include<algorithm>
#define fi first
#define se second
#define pb push_back
#define db double
//#define int long long //卡空间了
#define PII pair<int,int >
#define mem(a,b) memset(a,b,sizeof(a))
#define IOS std::ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);

using namespace std;
const int mod = 1e9+7,inf = 1e9;
const int N = 2e5+10,M = 1e6+10;

struct seg_max{
	#define node_l node<<1
	#define node_r node<<1|1
	struct node{
		int L,R;
		int minn;
		int laz;
	}t[4*M];
	void pushup(int node)
	{
		t[node].minn = min(t[node_l].minn,t[node_r].minn);
	}
	void pushdown(int node)
	{
		int laz = t[node].laz;
		if(laz!=inf)
		{
			t[node_l].laz = min(t[node_l].laz,laz);
			t[node_l].minn = min(t[node_l].minn,laz);
			t[node_r].laz = min(t[node_r].laz,laz);
			t[node_r].minn = min(t[node_r].minn,laz);
			t[node].laz = inf;
		}
	}
	void build(int node,int l,int r)
	{
		t[node].L = l,t[node].R = r;
		t[node].minn = inf;t[node].laz = inf;
		if(l==r) return ;
		int mid = (l+r)>>1;
		build(node_l,l,mid);build(node_r,mid+1,r);
		pushup(node);
	}
	void update(int node,int l,int r,int value)
	{
		if(t[node].L>=l&&t[node].R<=r)
		{
			t[node].minn = min(t[node].minn,value);
			t[node].laz = min(t[node].laz,value);
			return ;
		}
		pushdown(node);
		int mid = (t[node].L+t[node].R)>>1;
		if(r<=mid) update(node_l,l,r,value);
		else if(l>mid) update(node_r,l,r,value);
		else update(node_l,l,mid,value),update(node_r,mid+1,r,value);
		pushup(node);
	}
	int query(int node,int l,int r)
	{
		if(t[node].L>=l&&t[node].R<=r) return t[node].minn;
		pushdown(node);
		int mid = (t[node].L+t[node].R)>>1;
		if(r<=mid) return query(node_l,l,r);
		else if(l>mid) return query(node_r,l,r);
		else return min(query(node_l,l,mid),query(node_r,mid+1,r));
		pushup(node);
	}
}t_min;

int T,n,m,k;
PII va[N];
int dp[M];

bool cmp(PII A,PII B)
{
	return A.se<B.se;
}

signed main()
{
	IOS;
	cin>>n>>m;
	t_min.build(1,0,m); //注意建树的范围,会用到的点都要加上
	for(int i=1;i<=n;i++) cin>>va[i].fi>>va[i].se;
	sort(va+1,va+1+n,cmp);
	for(int i=0;i<=m;i++) dp[i] = inf;
	dp[0] = 0; //初始化
	t_min.update(1,0,0,0); //初始化
	for(int i=1;i<=n;i++)
	{
		int l = va[i].fi,r = va[i].se;
		dp[r] = min(dp[r],t_min.query(1,l-1,r)+1);
		t_min.update(1,r,r,dp[r]); //更新这个点的最小值即可。
	}
	if(dp[m]==inf) cout<<-1;
	else cout<<dp[m];
	return 0;
}

2020沈阳-The Boomsday Project

题意:
就是小A会借自行车,给你m个a和b,在第a天借b次。sum(b)<=1e5。然后借一次的价格是k,不过还有n<=500种会员卡,每个会员卡有d天时效,一共k次免费借车,花费c元。现在问你在满足小A借车的情况下花费最少是多少。

思考:

  1. 当时看完感觉不太好做,因为又陷入了那种思想,就是看到第a天借b次,并且一张会员卡有d天时效,k次免费。我怎么去用这些会员卡呢,可能这个卡用了可以对后面的有用,所以就傻逼的感觉不能写了。
  2. 虽然当时我看到sum(b)<=1e5,并且n一共就500个会员卡,可以两种循环,但是dp怎么dp呢?我感觉这不能dp,而且赛时过的也很少,感觉可能是牛逼算法?然后就没看了。
  3. 其实这题和之前做的蓝桥杯的一样:2022蓝桥杯国赛B组-费用报销。把天数都展开,放到数组里,这样就可以对天数进行dp了,反正每天用一次。然后转移的时候,枚举每个物品,看看dp[i]要从哪里转移,如果每次都暴力转移点j,这样复杂度太高了。由于dp的递增性,所以尽量选择越左边的越好,所以有单调性那么完全可以二分或者用个指针idx,如果二分复杂度会高,直接双指针就可以了。
  4. 不过有一点一直卡我的就是,我就想枚举物品,然后枚举m天去dp,反正我每个物品都去更新你一遍就可以了。但是WA24,我就想怎么会错呢。实际上,比如枚举第i个物品的时候,第j个物品还没用过,现在不能保证我枚举的双指针的dp是最小的那个,因为第j个物品还没用,所以此时i之前每个点的dp,都不是真正的dp值,因为后面还会变。所以必须先枚举天数,再枚举物品,让第i天的dp值确定下来,为后面的人去用的时候这个dp就固定了,就可以双指针单调走了。

代码:

#include<bits/stdc++.h>
#define fi first
#define se second
#define pb push_back
#define db double
#define int long long
#define PII pair<int,int >
#define mem(a,b) memset(a,b,sizeof(a))
#define IOS std::ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);

using namespace std;
const int mod = 1e9+7,inf = 1e18;
const int N = 1e6+10,M = 510;

int T,n,m,k;
int va[M],vb[M],vc[M];
int vd[N],cnt;
int idx[N],dp[N];

signed main()
{
	IOS;
	cin>>n>>m>>k;
	for(int i=1;i<=n;i++) cin>>va[i]>>vb[i]>>vc[i];
	for(int i=1;i<=m;i++)
	{
		int a,b;
		cin>>a>>b;
		while(b--) vd[++cnt] = a;
	}
	sort(vd+1,vd+1+cnt);
	for(int i=1;i<=cnt;i++) dp[i] = inf;
	for(int i=1;i<=cnt;i++)
	{
		dp[i] = min(dp[i],dp[i-1]+k);
		for(int j=1;j<=n;j++)
		{
			while(idx[j]+1<i&&vd[idx[j]+1]<=vd[i]-va[j]) idx[j]++;
			while(idx[j]+1<i&&i-(idx[j]+1)>=vb[j]) idx[j]++;
			dp[i] = min(dp[i],dp[idx[j]]+vc[j]);
		}
	}
	cout<<dp[cnt];
	return 0;
}

2022江苏省赛-Jump and Treasure

题意:
就是给你n个点,然后每个点有正的金币和负的金币,每次你跳跃的距离不能超过p,然后问你跳超过n的时候,最多能有多少金币。如果跳不出n,那么输出-1。然后给你q次查询,每次给你个x,然后你只能跳到是x的倍数的点上,然后请你输出答案。

思考:
一看n很大,查询也很大,不用想肯定预处理。然后每次查询i,那么一共n个i直接每个枚举一遍,nlogn复杂度。然后对于子问题,如何处理呢,这种求最大值的。定义dp[i]一般有两种定义状态:1.为走到i点的最大值为多少,仅仅是走到这个点,并不是这个点是前面中的最大的(这种dp[i]的定义呢,一般都是走到i点,是必选i点。一般这种题都是i转移的时候,不能从前面任意一点转移,而是从一些点转移有了限制,所以就不能代表前面所有的dp。这种题目就像最长上升子序列)。2.走到i点的最大值是多少,i点的最大值,就是前面所有状态的最大值而这种题目,一般都是可以从前面的任意一点来转移,随便转移,可以代表前面所有的状态。这种题目就像背包
由于这个题目说的是跳过n点,所以必须走到某一点,而不是停留在前面,所以就是第一种,当为第一种的时候,dp[i]从前面转移的时候,肯定就是所有可能的点都要枚举一下,这里有个跳跃距离的限制,所以更新的时候可以用个单调队列优化。

插曲:当时现场赛,读完题目肯定是预处理,对每个数字i都算一遍复杂度nlogn。然后让你求金币最大最小,直接dp,然后转移就是该怎么转移。当时我想到用优先队列,求区间最大值。但是优先队列复杂度加了个logn,所以nlognlogn过不了。然后于说可以用单调队列。这个我一直没学,所以当时WA了好几发。还是把所有的基础算法学学吧。

代码:

#include<bits/stdc++.h>
#define fi first
#define se second
#define pb push_back
#define db double
#define int long long
#define PII pair<int,int >
#define mem(a,b) memset(a,b,sizeof(a))
#define IOS std::ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
		
using namespace std;
const int mod = 1e9+7,inf = 1e18;
const int N = 1e6+10,M = 2010;

int T,n,m,k;
int va[N];
int dp[N]; //走到这个i点的最小花费,选择i点。
int anw[N];

int q[N],hh,tt;

bool check1(int a,int b)
{
	if(a+m<b) return true;
	return false;
}

bool check2(int a,int b)
{
	if(b>=a) return true;
	return false;
}

int solve(int x)
{
	if(x>m) return -1;
	hh = 0,tt = -1;
	q[++tt] = 0;
	for(int i=x;i<=n;i+=x)
	{
		while(hh<=tt&&check1(q[hh],i)) ++hh;
		dp[i] = dp[q[hh]]+va[i]; //这里先更新,你才能把dp[i]放进去比较
		while(hh<=tt&&check2(dp[q[tt]],dp[i])) --tt;
		q[++tt] = i;
	}
	while(hh<=tt&&check1(q[hh],n+1)) ++hh;
	return dp[q[hh]];
}

signed main()
{
	IOS;
	cin>>n>>k>>m;
	for(int i=1;i<=n;i++) cin>>va[i];
	for(int i=1;i<=n;i++) anw[i] = solve(i);
	while(k--)
	{
		int x;
		cin>>x;
		if(anw[x]==-1) cout<<"Noob\n";
		else cout<<anw[x]<<"\n";
	}
	return 0;
}

多重背包问题III

题意:
就是多重背包求最大值。

思考:
1.最简单的做法就是三重循环dp[i][j]为用到第i个,不超过j,最大价值。不能省略第一维,因为第二层枚举用多少个的时候,第一次是用的上一层的dp,但是第二次就可能用了这一层的dp,所以就不对了。
2.进一步优化可以把物品分类,因为2的次幂可以组成任意数,所以把物品分类后,进行01背包即可.
3.然后就是单调队列做法,其实就是把整个dp呢,看成好几个不同的dp,根据余数r来分的。对于每一种,转移的时候肯定也是从上一层来转移,所以,把上一层看成一格数组。然后每次取出最大值,来更新当前的dp。但是不同的j又加的数不同,所以又弄了一个偏移量,根据当前的j是多少来的。最重要的就是把他看成很多个dp。对于为啥有些题目用经典dp做的时候,需要用单调队列优化,因为,i从前面转移的时候,不能从1到i里面找最优值。因为题目会给你限制,让你从一段区间里面找,就是离i有一定的范围才行。所以dp[i]的状态,有时候是代表的从1到i最优的肯定在最后面。但是有时候最优的可能是在中间。这个想好就行了。如果在可能在中间那么要单调队列,如果就是在最近的,直接从最近的指针来转移就好了。

代码:

#include<bits/stdc++.h>
#define fi first
#define se second
#define pb push_back
#define db double
#define int long long
#define PII pair<int,int >
#define mem(a,b) memset(a,b,sizeof(a))
#define IOS std::ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);

using namespace std;
const int mod = 1e9+7,inf = 1e18;
const int N = 2e5+10,M = 2010;

int T,n,m,k;
int va[N];
int dp[N];
int tmp[N];

int q[N],hh,tt;

signed main()
{
    IOS;
    cin>>n>>m;
    for(int i=1;i<=n;i++)
    {
        int a,b,c;
        cin>>a>>b>>c;
        memcpy(tmp,dp,sizeof(dp)); //由于这个tmp中的值是不边的,但是不同的j转移的时候加的值不一样,所以这里弄了一个偏移量,加的值是根据转移多大来的。
        for(int r=0;r<a;r++)
        {
            hh = 0,tt = -1;
            for(int j=r;j<=m;j+=a)
            {
                while(hh<=tt&&q[hh]<j-c*a) ++hh; //如果开头的已经不能更新后面的了,就是滑动窗口要滑动了
                while(hh<=tt&&tmp[q[tt]]-(q[tt]-r)/a*b<=tmp[j]-(j-r)/a*b) --tt; //如果这个点能往前走就往前走。因为开头是最牛逼的。
                q[++tt] = j;
                dp[j] = tmp[q[hh]]+(j-q[hh])/a*b;
            }
        }
    }
    cout<<dp[m];
    return 0;
}

牛客多校-Two Frogs

题意:
就是现在有n个荷叶,你在第i个荷叶上可以跳到[i+1,i+va[i]]这段区间的荷叶上。当你跳到最后一个荷叶上就结束了。现在小A随机的跳,小B也随机的跳,问你他俩跳到n号荷叶上用的相同步数的期望。

思考:

  1. 首先既然小A和小B都是随机的,那么他俩的概率和期望步数都一样。刚开始我以为是求步数的期望,那么E[a] = p[a]×[a]×a。那么可以先求概率,或者直接求出来一个人的E1[a] = p[a]×a。那么两个人的E[a] = E1[a]×E1[a]/a就可以了。但是求出来发现不对,其实是是否相同的期望,也就是如果相同就是1,不相同就是0。那么就是求走到n点i步的概率平方之和。
  2. 然后我就直接dp[i][j]为走到i点走j步的概率,但是这样是三重循环,第一重走到哪个荷叶,第二重为步数,第三重是这个i点可以从前面的哪些点转移。这里就卡了会,我总感觉这种做法太经典了,能优化,就像之前做的这个:Chip Move。但是一想那个是只更新某个点,这个是更新一段区间。其实一样,这个更新一段区间可以先更新差分,然后再求一个前缀和就可以了。
  3. 也就是定义dp[i]为走到i的概率。先枚举步数,因为步数肯定是递增的,然后枚举每个点会对下一层做的贡献,那么就是对下一层的一段区间做贡献,先维护个差分,处理完之后。对差分求前缀和那么现在就是下一层到达每个点的概率。那么再复制给dp就行了。那么很明显就是一个滚动数组,因为这一层的只会用上一层的。答案加的时候就在每一层到达n号点的概率都求个平方就可以了。

代码:

#include<bits/stdc++.h>
#define int long long
#define IOS std::ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);

using namespace std;

const int N = 8e3+10,mod = 998244353;

int n;
int va[N];
int dp[N],tp[N];
int sum[N];

int ksm(int a,int b)
{
	int sum = 1;
	while(b)
	{
		if(b&1) sum = sum*a%mod;
		a = a*a%mod;
		b >>= 1;
	}
	return sum;
}

signed main()
{
	IOS;
	cin>>n;
	for(int i=1;i<n;i++)
	{
		cin>>va[i];
		sum[i] = ksm(va[i],mod-2)%mod; //记得预处理下要不然就超时了。
	}
	dp[1] = 1;
	int ans = 0;
	for(int j=1;j<=n-1;j++)
	{
		for(int i=0;i<=n;i++) tp[i] = 0;
		for(int i=1;i<n;i++)
		{
			int l = i+1,r = i+va[i];
			tp[l] = (tp[l]+dp[i]*sum[i]%mod)%mod;
			tp[r+1] = (tp[r+1]-dp[i]*sum[i]%mod)%mod;
		}
		for(int i=1;i<=n;i++) tp[i] = (tp[i]+tp[i-1])%mod;
		for(int i=1;i<=n;i++) dp[i] = tp[i];
		ans = (ans+dp[n]*dp[n]%mod)%mod; //走了j步到达n号点的概率
	}
	ans = (ans%mod+mod)%mod;
	cout<<ans;
	return 0;
}
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值