「一本通」单调队列优化dp学习笔记

版权声明:本文为QQQQqtt原创,但是我猜没有人转载oAo https://blog.csdn.net/qq_36038511/article/details/82854320

总结:

题目一般要求由前面的一个状态得出当前的最优状态,满足dp,但如果暴力查找前一个决策,复杂度显然不可以接受。这时候可以用一个能从两端删除但只能从一段添加的单调队列及时把不可能的决策排除掉,然后再把当前的决策插进去,保持队列中的单调性。然后就乱搞。(表示进阶上的题的证明好强满足等式右边的单调性后还要加set优化f数组)


loj#10175. 「一本通 5.5 例 1」滑动窗口

https://loj.ac/problem/10175
代码略(ju)丑

#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
struct node
{
	int x,c;
	node()
	{
		c=-999999999;
		x=0;
	}
}lmn[1100000],lmx[110000];
int mn[1100000],mx[1100000];
int main()
{
	int n,k;
	scanf("%d%d",&n,&k);
	int c,hmn=1,tmn=0,hmx=1,tmx=1;
	scanf("%d",&c);
	lmn[++tmn].x=1;lmn[tmn].c=c;lmx[++tmx].x=1;lmx[tmx].c=c;
	for (int i=2;i<=n;i++)
	{
		scanf("%d",&c);
		while (hmn<=tmn&&lmn[tmn].c>c) tmn--;
		lmn[++tmn].x=i;lmn[tmn].c=c;
		while (hmn<=tmn&&i-lmn[hmn].x+1>k) hmn++;
		
		while(hmx<=tmx&&lmx[tmx].c<c) tmx--;
		lmx[++tmx].x=i;lmx[tmx].c=c;
		while (hmx<=tmx&&i-lmx[hmx].x+1>k) hmx++;
		
		if (i>=k)
		{
			mn[i-k+1]=lmn[hmn].c;
			mx[i-k+1]=lmx[hmx].c;
			
		}
	}
	for (int i=1;i<=n-k+1;i++) printf("%d ",mn[i]);
	printf("\n");
	for (int i=1;i<=n-k+1;i++) printf("%d ",mx[i]);
	printf("\n");
	return 0;
}

loj#10176. 「一本通 5.5 例 2」最大连续和

https://loj.ac/problem/10176
将求区间和转换为前缀和相减 (s[i]-s[j],j<i)
对于一个s[i],s[j]越小区间和越大
所以维护一个单调上升的序列(维护队头是最小最优决策),同时统计答案

#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
struct node
{
	int x,c;
}list[210000];
int s[210000],a[210000];
int main()
{
	int n,m;
	scanf("%d%d",&n,&m);
	for (int i=1;i<=n;i++)
	{
		scanf("%d",&a[i]);
		s[i]=s[i-1]+a[i];
	}
	//维护一个严格单调上升序列 
	int head=0,tail=0,ans=-2147483647;
	list[0].x=0;list[0].c=0;
	for (int i=1;i<=n;i++)
	{
		while (head<=tail&&i-list[head].x>m) head++; //选list[head].x+1 ~ i 
		ans=max(s[i]-list[head].c,ans);
		while (head<=tail&&list[tail].c>s[i]) tail--;
		list[++tail].c=s[i]; list[tail].x=i;
	}
	printf("%d\n",ans);
	return 0;
}

loj#10177. 「一本通 5.5 例 3」修剪草坪

https://loj.ac/problem/10177
书上写的解法是很dp的做法,f[i][0],f[i][1]代表第i只牛选或不选的最大效率,用单调队列优化决策,维护一个单调下降的序列
我的的是用总和减去不选的奶牛的效率……
将问题转化为每k+1个数字中必须选择一个,使选出的总和最小

#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
struct node
{
	int x;long long c; //x:最后一只的位置 
}list[110000];
int a[110000];
long long s[110000],f[110000];
int main()
{
	int n,k;
	scanf("%d%d",&n,&k);
	for (int i=1;i<=n;i++)
	{
		scanf("%d",&a[i]);
		s[i]=s[i-1]+a[i];
	}
	//维护一个严格单调递增序列 
	int head=0,tail=0;
	list[0].x=0;list[0].c=0;
	for (int i=1;i<=n;i++)
	{
		while (head<=tail&&i-list[head].x>k+1) head++;
		if (i<=k+1) f[i]=a[i]; else f[i]=list[head].c+a[i];
		while (head<=tail&&list[tail].c>f[i]) tail--;
		list[++tail].c=f[i]; list[tail].x=i;
	}
	/*long long ans=2147483647;
	for (int i=1;i<=n;i++) ans=min(ans,f[i]);*/
	while (head<=tail&&n-list[head].x>k) head++;
	printf("%lld\n",s[n]-list[head].c);
	return 0;
}

loj#10178. 「一本通 5.5 例 4」旅行问题

https://loj.ac/problem/10178
用油-路程,即可判断这一段路能否走通,求前缀和后即可判断起点到i能否走通
判断是否能走通,即判断i~i+n是否有负数
直接判复杂度为 N 2 N^2 ,显然是无法接受的
考虑将问题转化为例1,找到i~i+n里面的最小值,判断最小值是否小于零
至于顺逆时针……还是建议画个图感受一下位置和数字的关系吧2333

#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
#define LL long long
struct node
{
	LL c;int x;
}lista[210000],listb[210000];
LL disa[210000],disb[210000],gasa[210000],gasb[210000],a[210000],b[210000];
bool ans[210000];
int main()
{
	int n;
	scanf("%d",&n);
	for (int i=1;i<=n;i++)
	{
		scanf("%d%d",&gasa[i],&disa[i]); 
		disa[n+i]=disa[i];
		//disb[n-i+1]=disa[i]; disb[n-i+1+n]=disa[i];
		gasa[n+i]=gasa[i];
		//gasb[n-i+1]=gasa[i]; gasb[n-i+1+n]=gasa[i];
	}
	for (int i=1;i<=2*n;i++) 
	{
		a[i]=gasa[i]-disa[i]+a[i-1];
		b[i]=gasa[2*n-i+1]-disa[2*n-i]+b[i-1];
	}
	/*
	维护一个单调上升序列  
	找到i~i+n-1内最小值  最小值-自己>=0 一定区间内没有负数 
	注意顺逆时针的对应关系 */
	int heada=1,taila=0,headb=1,tailb=0;
	lista[1].x=0; lista[1].c=0;
	listb[1].x=0; listb[1].c=0;
	for (int i=1;i<=2*n;i++)
	{
		while (heada<=taila&&i-lista[heada].x>n) heada++;
		while (heada<=taila&&a[i]<=lista[taila].c) taila--;
		lista[++taila].c=a[i]; lista[taila].x=i;
		while (headb<=tailb&&i-listb[headb].x>n) headb++;
		while (headb<=tailb&&b[i]<=listb[tailb].c) tailb--;
		listb[++tailb].c=b[i]; listb[tailb].x=i;
		if (i>=n)
		{
			if (lista[heada].c-a[i-n-1]>=0) ans[i-n]=1;
			if (listb[headb].c-b[i-n-1]>=0) ans[n-(i-n)+1]=1;
		}
	} 
	for (int i=1;i<=n;i++) if (ans[i]==1) printf("TAK\n");	else printf("NIE\n");
	return 0;
}

#10179. 「一本通 5.5 例 5」Banknotes

https://loj.ac/problem/10179

思路&题解:

https://blog.csdn.net/WWWengine/article/details/82187471
完全不知道书上在讲什么.jpg
bzoj居然把POI的题解开了权限,感动

#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
struct node
{
	int c,x;
}list[21000];
int b[21000],c[21000],f[210000]; 
int main()
{
	int n,k;
	scanf("%d",&n);
	for (int i=1;i<=n;i++) scanf("%d",&b[i]);
	for (int i=1;i<=n;i++) scanf("%d",&c[i]);
	scanf("%d",&k);
	/*
	把要凑出的值k 对 bi取模 然后按模数分组 
 	设模数为 m ,有方程式 f[bi*j+m]=min{f[bi*u+m]+(j-u)} (j<=ci,u<j)
 	改一改就是f[bi*j+m]=min{f[bi*u+m]-u}+j;
 	可以知道是j单调上升时f也是单调上升的
 	用单调队列维护一个单调上升的序列 每次取出队头求值 
	*/ 
	memset(f,63,sizeof(f));
	f[0]=0;
	for (int i=1;i<=n;i++)
	{
		for (int m=0;m<b[i];m++)//枚举余数 
		{
			int head=1,tail=0;
			list[1].x=0;list[1].c=0;
			for (int j=0;;j++)//枚举倍数 
			{
				int x=b[i]*j+m;
				if (x>k) break;
				while (head<=tail&&j-list[head].x>c[i]) head++;
				while (head<=tail&&list[tail].c>f[x]-j) tail--;
				list[++tail].x=j; list[tail].c=f[x]-j;
				if (list[head].c+j<f[x]) f[x]=list[head].c+j;
			}
		}
	}
	printf("%d\n",f[k]);
	return 0;
}

loj#10180. 「一本通 5.5 练习 1」烽火传递

https://loj.ac/problem/10180
每m个数字中至少要取1个,使取出的总和最小

#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
struct node
{
	int x,c;
}list[210000];
int a[210000],f[210000];
int main()
{
	int n,m;
	scanf("%d%d",&n,&m);
	for (int i=1;i<=n;i++)
		scanf("%d",&a[i]);
	memset(f,63,sizeof(f));f[0]=0;
	a[++n]=0;
	int head=1,tail=1;
	list[1].x=list[1].c=0;
	for (int i=1;i<=n;i++)
	{
		while (head<=tail&&i-list[head].x+1>m+1) head++;
		f[i]=min(list[head].c+a[i],f[i]);
		while (head<=tail&&list[tail].c>=f[i]) tail--;
		list[++tail].c=f[i]; list[tail].x=i;
	}
	printf("%d\n",f[n]);
	return 0;
}

loj#10181. 「一本通 5.5 练习 2」绿色通道

https://loj.ac/problem/10181
设k为最长的空题数
问题转换为每k+1个数字至少要取一个数,取出的数字之和小于等于t
二分枚举k即可

#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
struct node
{
	int x,c;
}list[51000];
int a[51000],t,n,f[51000];
bool check(int k)
{
	memset(f,63,sizeof(f)); f[0]=0;k++;
	int head=1,tail=1;
	list[1].x=list[1].c=0; 
	for (int i=1;i<=n;i++)
	{
		while (head<=tail&&i-list[head].x>k) head++;
		f[i]=list[head].c+a[i];
		while (head<=tail&&list[tail].c>f[i]) tail--;
		list[++tail].x=i;list[tail].c=f[i];
	}
	return f[n]<=t;
}
int main()
{
	scanf("%d%d",&n,&t);
	for (int i=1;i<=n;i++) scanf("%d",&a[i]);
	int l=0,r=n,ans=0; a[++n]=0;
	while (l<=r)
	{
		int mid=(l+r)/2;
		if (check(mid)) {r=mid-1; ans=mid;}
		else l=mid+1;
	}
	printf("%d\n",ans);
	return 0;
}

loj#10182. 「一本通 5.5 练习 3」理想的正方形

https://loj.ac/problem/10182
二维的单调队列???
其实是把二维拍扁了变一维吧
先把每一行都做一遍滑动窗口
根据每一行的答案按每一列再做一次滑动窗口

#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
struct node
{
	int x,c;
}list[1100],list1[1100],list2[1100];
int s[1100][1100],a,b,n,mn[1100][1100],mx[1100][1100]; 
void dp()
{
	//每一行的最大/小 
	for (int i=1;i<=a;i++)
	{
		int head=1,tail=0;
		list[1].x=list[1].c=0;
		for (int j=1;j<=b;j++)
		{
			while (head<=tail&&j-list[head].x+1>n) head++;
			while (head<=tail&&list[tail].c>=s[i][j]) tail--;
			list[++tail].x=j; list[tail].c=s[i][j];
			mn[i][j]=list[head].c;
		}
		head=1;tail=0;
		list[1].x=list[1].c=0;
		for (int j=1;j<=b;j++)
		{
			while (head<=tail&&j-list[head].x+1>n) head++;
			while (head<=tail&&list[tail].c<=s[i][j]) tail--;
			list[++tail].x=j; list[tail].c=s[i][j];
			mx[i][j]=list[head].c;
		}
	}
}
int main()
{
	memset(mn,63,sizeof(mn));
	scanf("%d%d%d",&a,&b,&n);
	for (int i=1;i<=a;i++)
	{
		for (int j=1;j<=b;j++) scanf("%d",&s[i][j]);
		mn[i][0]=0;
	}
	dp();
	int ans=2147483647;
	//每一列
	for (int j=n;j<=b;j++)
	{
		int head1=1,tail1=0;
		list1[1].x=list1[1].c=0;
		int head2=1,tail2=0;
		list2[1].x=list2[1].c=0;
		for (int i=1;i<=a;i++)
		{
			while (head1<=tail1&&i-list1[head1].x+1>n) head1++;
			while (head1<=tail1&&list1[tail1].c>=mn[i][j]) tail1--;
			list1[++tail1].x=i; list1[tail1].c=mn[i][j];
			while (head2<=tail2&&i-list2[head2].x+1>n) head2++;
			while (head2<=tail2&&list2[tail2].c<=mx[i][j]) tail2--;
			list2[++tail2].x=i; list2[tail2].c=mx[i][j];
			if (i>=n) ans=min(ans,list2[head2].c-list1[head1].c);
		}
	}
	printf("%d\n",ans);
	return 0;
}

loj#10183. 「一本通 5.5 练习 4」股票交易

https://loj.ac/problem/10183

题解&思路:

https://blog.csdn.net/wzq_qwq/article/details/46410395
https://blog.csdn.net/Rose_max/article/details/81805185
设f[第i天][持有的股票] p=i-w-1
这一天啥都不干的时候: f [ i ] [ j ] = f [ i 1 ] [ j ] f[i][j]=f[i-1][j]
买入股票:
f [ i ] [ j ] = j A s i &lt; = k &lt; j m a x ( f [ p ] [ k ] ( j k ) A p i ) f[i][j]=\sum_{j-Asi&lt;=k&lt;j}max(f[p][k]-(j-k)*Api)
把j拆出来:
f [ i ] [ j ] = j A s i &lt; = k &lt; j m a x ( f [ p ] [ k ] + k A p i ) j A p i f[i][j]=\sum_{j-Asi&lt;=k&lt;j}max(f[p][k]+k*Api)-j*Api
卖出股票:
f [ i ] [ j ] = j &lt; k &lt; = j + B p i m a x ( f [ p ] [ k ] + ( k j ) B p i ) f[i][j]=\sum_{j&lt;k&lt;=j+Bpi}max(f[p][k]+(k-j)*Bpi)
同样把j拆出来:
f [ i ] [ j ] = j &lt; k &lt; = j + B p i m a x ( f [ p ] [ k ] + k B p i ) j B p i f[i][j]=\sum_{j&lt;k&lt;=j+Bpi}max(f[p][k]+k*Bpi)-j*Bpi

可以看出f[ ][j]单调不减(要么啥都不干要么升值),用单调队列维护即可

#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
int list[2100],ap[2100],bp[2100],as[2100],bs[2100],f[2100][2100];//f[第i天][持有的股票数量]=赚的钱 
int main()
{
	int t,maxp,w;
	scanf("%d%d%d",&t,&maxp,&w);
	for (int i=1;i<=t;i++) scanf("%d%d%d%d",&ap[i],&bp[i],&as[i],&bs[i]);
	memset(f,-63,sizeof(f));
	for (int i=1;i<=t;i++)
	{
		for (int j=1;j<=as[i];j++) f[i][j]=-j*ap[i]; //原来没有股票  先买入 
		for (int j=0;j<=maxp;j++) f[i][j]=max(f[i][j],f[i-1][j]);
		int p=i-1-w; if (p<0) continue;
		int head=1,tail=0;
		for (int j=0;j<=maxp;j++)
		{
			while (head<=tail&&list[head]<j-as[i]) head++; //最多只能买入as[i] 
			while (head<=tail&&f[p][list[tail]]+list[tail]*ap[i]<=f[p][j]+j*ap[i]) tail--;
			list[++tail]=j;
			f[i][j]=max(f[i][j],f[p][list[head]]-(j-list[head])*ap[i]);
		}
		head=1;tail=0;
		for (int j=maxp;j>=0;j--)
		{
			while (head<=tail&&list[head]>j+bs[i]) head++;//最多只能卖出bs[i] 
			while (head<=tail&&f[p][list[tail]]+list[tail]*bp[i]<=f[p][j]+j*bp[i]) tail--;
			list[++tail]=j;
			f[i][j]=max(f[i][j],f[p][list[head]]+(list[head]-j)*bp[i]);
		}
	} 
	int ans=0;
	for (int i=0;i<=maxp;i++) ans=max(ans,f[t][i]);
	printf("%d\n",ans);
	return 0;
}

(过了一把写公式的爽瘾)
完结撒花!

猜你喜欢

转载自blog.csdn.net/qq_36038511/article/details/82854320