第一章 常见优化技巧 例题

第一章 常见优化技巧 例题

T1 P1102 A-B数对

题目描述:

给出一串数以及一个数字C,要求计算出所有A-B=C的数对的个数。

n<=2e5

思路:

将数列排序,去重并统计每个数的出现次数。

对于每一个数a[l],用lower_bound寻找是否存在a[r]=a[l]+c,直到a[l]+c>a[cnt]为止。

Code:

#include<bits/stdc++.h>
#define ll long long
#define fr(i,r) for(int i=1;i<=r;++i)
#define For(i,l,r) for(int i=l;i<=r;++i)
#define Rof(i,r,l) for(int i=r;i>=l;--i)
using namespace std;
const int N=2e5+10,Inf=0x7fffffff;
char cch;
int res,zf;
inline int rd()
{
	while((cch=getchar())<45);
	if(cch^45)res=cch^48,zf=1;
	else res=0,zf=-1;
	while((cch=getchar())>=48)res=(res*10)+(cch^48);
	return res*zf;
}

int n=rd(),c=rd();
int l,r;
struct Node
{
	int cnt,num;
	bool operator <(const Node& x)const{return num<x.num;}
	bool operator <(const int& x)const{return num<x;}
}a[N];

int cnt;
ll ans;
int main()
{
	fr(i,n) a[i].num=rd();
	sort(a+1,a+n+1);
	for(int i=1;i<=n;)//去重+计数
	{
		a[++cnt].cnt=1,a[cnt].num=a[i].num;
		while(a[++i].num==a[cnt].num)++a[cnt].cnt;
	}
	while(1)
	{
		r=lower_bound(a+1,a+cnt+1,a[++l].num+c)-a;
		if(r>cnt)break;//数列中最大值小于a[l]+cnt,那后面的也都不符合,退出
		if(a[r].num==a[l].num+c) ans+=(ll)a[r].cnt*a[l].cnt;
	}
	cout<<ans;
	return 0;
}

T2 P1638 逛画展

题目描述:

有n个取值1~m的数组成一个数列,求一个最短区间[a,b]使该区间内的数取到1~m中的所有值。

n<=1e6,m<=1e3

思路1: O(nlogn) 二分答案

二分答案len值(b-a+1),然后统计在长为len的区间内,每个画家的画出现了多少次,区间内出现了几个画家的画,不断右移区间直至所有画家均出现或移到最后也不符合。

一次检验O(n),总共log(n)次,总时间复杂度O(nlogn)。

思路2: O(n) 双指针

检验一个区间是否合法的方法同上。

先将右指针右移至区间合法,将左边不断弹出直至最左端画作的画家的cnt为1(再多退一位就不合法),统计区长最小值即可。然后右指针右移一位,尽可能退左边。循环至右指针移到最后。

Code: (思路1)

#include<bits/stdc++.h>//二分答案len值,O(1)检验, O(nlogn)   //two-pointers可以做到O(n),检验方法一致,每次右指针右移一位后将左边不断弹出至最左端画作的画家的cnt为1,统计区长最小值即可
#define ll long long
#define fr(i,r) for(int i=1;i<=r;++i)
#define For(i,l,r) for(int i=l;i<=r;++i)
#define Rof(i,r,l) for(int i=r;i>=l;--i)
using namespace std;
const int N=1e6+10,M=2e3+10,Inf=0x7fffffff;
char cch;
int res,zf;
inline int rd()
{
	while((cch=getchar())<45);
	if(cch^45)res=cch^48,zf=1;
	else res=0,zf=-1;
	while((cch=getchar())>=48)res=(res*10)+(cch^48);
	return res;
}

int n=rd(),m=rd(),al,ar;
int mid,l=1,r=n;
int a[N]/*储存画作*/,cnt[M]/*每个画家的画在区间内的出现次数*/,avai/*存在画作的画家个数*/;

inline void init()
{
	memset(cnt,0,sizeof(cnt));
	avai=0;
}
int main()
{
	fr(i,n) a[i]=rd();
	while(l<r)
	{
		init();
		mid=(l+r+1)>>1;
		fr(i,mid)
		{
			if(!cnt[a[i]])++avai;
			++cnt[a[i]];
		}
		if(avai==m)//如果条件满足(包含所有画家的画作)
		{
			r=mid-1;
			al=1;
			ar=mid;
			continue;
		}
		
		fr(i,n-mid)
		{
			--cnt[a[i]];
			if(!cnt[a[i]])--avai;
			
			if(!cnt[a[i+mid]])++avai;
			++cnt[a[i+mid]];
			if(avai==m)//如果条件满足(包含所有画家的画作)
			{
				r=mid-1;
				avai=0;//顺便承担jud的作用
				al=i+1;
				ar=mid+i;
				break;
			}
		}
		if(avai) l=mid+1;//avai>0则说明条件始终没有得到满足
	}
	cout<<al<<' '<<ar;
	return 0;
}

*T3 P1115 最大子段和

题目描述:

给出一个长度为n的序列a,选出其中连续的一段使得这段和最大。

n<=2e5 , -1e4<=ai<=1e4

思路:

dp

对于每个数,采用局部贪心策略,要么重新计和,要么累加,最后取所有dp值中的最大值。

Code:

#include<bits/stdc++.h>//dicussed
#define fr(i,r) for(int i=1;i<=r;++i)
using namespace std;
const int N=2e5+10,Inf=0x7fffffff;
int n,dp[N],mx=-Inf,k;
int main()
{
	scanf("%d",&n);
	fr(i,n) scanf("%d",&k),dp[i]=max(dp[i-1]+k,k),mx=max(mx,dp[i]);
	cout<<mx;
}

*T4 P7072 [CSP-J2020] 直播获奖

题目描述:

给定w,依次给定n个整数,给出第p个数时询问第 \(max(1,\left \lfloor p*w\% \rfloor \right)\) 个数的大小

n<=1e5

思路:

双优先队列

考虑到本题每新加入一个数,只会涉及到临界点最多一位的更改。因此开两个优先队列,一个小根堆存前 \(max(1,\left \lfloor p*w\% \rfloor \right)\) 个数,堆顶数即为临界点上的数;另一个开大根堆,存储不符合要求的数,堆顶即为临界点的后一位。这样,每加入一个新的数时,只涉及到 小根堆堆顶、大根堆堆顶、新数三个数的修改,分情况讨论即可。详见代码。

Code:

#include<bits/stdc++.h>//asked
#define ll long long
#define fr(i,r) for(int i=1;i<=r;++i)
#define For(i,l,r) for(int i=l;i<=r;++i)
#define Rof(i,r,l) for(int i=r;i>=l;--i)
using namespace std;
const int N=2e5+10,Inf=0x7fffffff;
char cch;
int res,zf;
inline int rd()
{
	while((cch=getchar())<45);
	if(cch^45)res=cch^48,zf=1;
	else res=0,zf=-1;
	while((cch=getchar())>=48)res=(res*10)+(cch^48);
	return res;
}

int n=rd(),w=rd(),a;
int rew;
priority_queue<int,vector<int>,greater<int> > q1;
int t1;
priority_queue<int> q2;
int t2;

int main()
{
	q1.push(Inf),q2.push(0);
	fr(i,n)
	{
		a=rd();
		if(rew<max(1,i*w/100))
		{
			rew=max(1,i*w/100);
			if(a<q2.top())
			{
				q1.push(q2.top());
				q2.pop();
				q2.push(a);
			}
			else q1.push(a);
		}
		else
		{
			if(a>q1.top())
			{
				q2.push(q1.top());
				q1.pop();
				q1.push(a);
			}
			else q2.push(a);
		}
		cout<<q1.top()<<' ';
	}
	return 0;
}

**T5 P2671 [NOIP2015 普及组] 求和

题目描述:

给定n个数,有m种颜色,然后依次给定n个数的颜色和数值。

求所有颜色相同的且奇偶性相同的数的分数之和%mod,i和j的分数定义为\(v_{i,j}=(i+j)*(number_i+number_j)\)

n<=1e5,m<=1e5,mod=10007

思路:

先按照颜色和奇偶性将原数列分为2m个组,这样就消除了限制条件,直接对每组分别处理即可。

通过找规律,可以发现对于每个组内的k个数:

\[\begin{align} &(找规律)\\ \\\\&k=1\qquad0 \\\\&k=2\qquad(x_1y_1+x_1y_2+x_2y_1+x_2y_2) \\\\&k=3\qquad(x_1y_1+x_1y_2+x_2y_1+x_2y_2)+(x_1y_1+x_1y_3+x_3y_1+x_3y_3)+(x_2y_2+x_2y_3+x_3y_2+x_3y_3) \\&\qquad\qquad={\color{Orange}2}x_1y_1+x_1y_2+x_1y_3+x_2y_1+{\color{Orange}2}x_2y_2+x_2y_3+x_3y_1+x_3y_2+{\color{Orange}2}x_3y_3 \\&\qquad\qquad=\sum_{i=1}^3x_i*\sum_{j=1}^3y_j+{\color{Orange}1}*\sum_{i=1}^3x_iy_i\\ \\\\&k=4\qquad(x_1y_1+x_1y_2+x_2y_1+x_2y_2)+(x_1y_1+x_1y_3+x_3y_1+x_3y_3)+(x_1y_1+x_1y_4+x_4y_1+x_4y_4)+(x_2y_2+x_2y_3+x_3y_2+x_3y_3)+(x_2y_2+x_2y_4+x_4y_2+x_4y_4)+(x_3y_3+x_3y_4+x_4y_3+x_4y_4) \\&\qquad\qquad=\sum_{i=1}^4\sum_{j=1}^4x_iy_j+{\color{Orange}2}*\sum_{i=1}^4x_iy_i \\&\qquad\qquad=\sum_{i=1}^4x_i*\sum_{j=1}^4y_j+{\color{Orange}2}*\sum_{i=1}^4x_iy_i\\\\\\ &\therefore\sum_{i=1}^k\sum_{j=i+1}^k(x_i+x_j)(y_i+y_j)\\ &=\sum_{i=1}^kx_i*\sum_{j=1}^ky_j+{\color{Orange}(k-2)}*\sum_{i=1}^kx_iy_i \end{align} \]

??严谨证明还不会

因此只需对于每个组线性求出组内所有数的序号和、数值和、序号与数值之积之和并统计项数,然后套公式计算即可。

Code:

#include<bits/stdc++.h>//asked (own method uncompleted)
#define fr(i,r) for(int i=1;i<=r;++i)
#define For(i,l,r) for(int i=l;i<=r;++i)
#define int long long
using namespace std;
const int N=1e5+10,mod=10007;
char cch;
int res,zf;
inline int rd()
{
	while((cch=getchar())<45);
	if(cch^45)res=cch^48,zf=1;
	else res=0,zf=-1;
	while((cch=getchar())>=48)res=(res*10)+(cch^48);
	return res;
}

int n=rd(),m=rd(),ans;
int a[N];
int cnt[N][2],sumx[N][2],sumy[N][2],num[N][2];
int k;

signed main()
{
	fr(i,n)a[i]=rd();
	fr(i,n)
	{
		k=rd();
		num[k][i&1]=(num[k][i&1]+a[i]*i)%mod;
		++cnt[k][i&1];
		sumx[k][i&1]=(sumx[k][i&1]+i)%mod;
		sumy[k][i&1]=(sumy[k][i&1]+a[i])%mod;
	}
	fr(i,m)
		For(j,0,1)
			ans=(ans+sumx[i][j]*sumy[i][j]%mod+(cnt[i][j]-2)*num[i][j]%mod)%mod;
	cout<<ans;
	return 0;
}

**T6 P4147 玉蟾宫

题目描述:

给定一个全由'F'或'R'构成的n*m矩阵,求面积最大的全标有'F'的矩阵的面积

思路1: 悬线法 O(n*m)

记录每个点到左边的最远可行点和最右可行点,以及向上的最远距离,再\(O(n^2)\)对每一个点求出它往上的最大面积即可。

??思路2: 单调栈 O(??)

同样的方法求向上最大延伸高度。

对于一行:对 向上高度 进行一次单调递增栈。每次弹栈,即出现高度变小时,更新最大面积。

Code: 思路1

#include<bits/stdc++.h>//searched
#define ll long long//悬线法:通常用于处理满足某个条件的最大矩阵
#define fr(i,r) for(int i=1;i<=r;++i)
#define For(i,l,r) for(int i=l;i<=r;++i)
#define Rof(i,r,l) for(int i=r;i>=l;--i)
using namespace std;
const int N=1010,Inf=0x7fffffff;
char cch;
int res,zf;
inline int rd()
{
	while((cch=getchar())<45);
	if(cch^45)res=cch^48,zf=1;
	else res=0,zf=-1;
	while((cch=getchar())>=48)res=(res*10)+(cch^48);
	return res;
}

int l[N][N],r[N][N],up[N][N];
bool mp[N][N];
int n=rd(),m=rd();
int ans;

int main()
{
	fr(i,n)fr(j,m)
	{
		while((cch=getchar())^'R'&&cch^'F');
		mp[i][j]=bool(cch^'R');
		l[i][j]=r[i][j]=j;//先置为自己
		up[i][j]=1;//向上最大距离置为1
	}
	
	For(i,1,n)For(j,2,m)//处理l:对于一个点(i,j)往左可以走到的最左的符合条件的点的位置
	{
		if(mp[i][j-1]&&mp[i][j])//如果均成立,l值延续
			l[i][j]=l[i][j-1];
	}
	For(i,1,n)Rof(j,m-1,1)//处理r:对于一个点(i,j)往右可以走到的最右的符合条件的点的位置
	{
		if(mp[i][j]&&mp[i][j+1])//如果均成立,l值延续
			r[i][j]=r[i][j+1];
	}
	
	fr(i,n)//单独处理第一行
		if(mp[1][i])
			ans=max(r[1][i]-l[1][i]+1,ans);
	For(i,2,n)For(j,1,m)//处理up:对于一个点(i,j)往上可以走到的最上的符合条件的点的< 距离 >
	{
		if(!mp[i][j])continue;//如果是障碍,不统计答案跳过
		if(mp[i-1][j])//up
		{
			up[i][j]=up[i-1][j]+1;//同理
			l[i][j]=max(l[i][j],l[i-1][j]);//顺便更新l,r的最小值(即这一列往左、往右分别能走到的位置的极限值)
			r[i][j]=min(r[i][j],r[i-1][j]);
		}
		ans=max(ans,(r[i][j]-l[i][j]+1)*up[i][j]);
	}
	
	cout<<ans*3;
	return 0;
}

T7 P2866 [USACO06NOV]Bad Hair Day S

题目描述:

给定n个数,求 每个数后面 连续递增数列长度 之和。

n<=8e4

思路:

单调栈板子题。

维护一个单调递增栈,并同时维护栈内元素个数,每次弹完栈后累加栈内剩余元素个数,再将新元素压栈即可。

Code:

#include<bits/stdc++.h>
#define ll long long
#define fr(i,r) for(int i=1;i<=r;++i)
#define For(i,l,r) for(int i=l;i<=r;++i)
#define Rof(i,r,l) for(int i=r;i>=l;--i)
using namespace std;
const int N=8e4+10,Inf=0x7fffffff;
char cch;
int res,zf;
inline int rd()
{
	while((cch=getchar())<45);
	if(cch^45)res=cch^48,zf=1;
	else res=0,zf=-1;
	while((cch=getchar())>=48)res=(res*10)+(cch^48);
	return res;
}

stack <int> s;
int n=rd(),h,ans,scnt;

int main()
{
	s.push(Inf);
	fr(i,n)
	{
		h=rd();
		while(s.top()<=h)
		{
			--scnt;
			s.pop();
		}
		ans+=scnt;
		++scnt;
		s.push(h);
	}
	cout<<ans;
	return 0;
}

*T8 P1950 长方形

题目描述:

思路:

code:

#include<bits/stdc++.h>//searched
#define ll long long
#define fr(i,r) for(int i=1;i<=r;++i)
#define For(i,l,r) for(int i=l;i<=r;++i)
#define Rof(i,r,l) for(int i=r;i>=l;--i)
using namespace std;
const int N=1e3+10,Inf=0x7fffffff;
char cch;
int res,zf;
inline int rd()
{
	while((cch=getchar())<45);
	if(cch^45)res=cch^48,zf=1;
	else res=0,zf=-1;
	while((cch=getchar())>=48)res=(res*10)+(cch^48);
	return res;
}

int n=rd(),m=rd();
bool mp[N][N];
int l[N],r[N],h[N];//h[i]:处理行第i列可向上延伸多少长度 l[i]&r[i]:对于h[i],其左边第一个<=h[i]和右边第一个<h[i]的数
int top,sta[N];//数组模拟栈
ll ans;

inline void workline()
{
	//求解l数组
	top=0;
	sta[0]=0;
	Rof(i,m,1)//逆序枚举,利用单调递增栈线性统计每个数左边第一个小于等于它的数的位置(即l)
	{
		while(h[sta[top]]>=h[i]) l[sta[top]]=i,--top;//左<=右,则右边那个点的h会被左边限制,更新右边点的l值,退栈
		sta[++top]=i;
	}
	while(top) l[sta[top]]=0,--top; //一遍走完,对于单调递增栈中剩下的数中的任意一个数p,它左边所有位1~p-1的h值都比它大,故没有数能限制它,将其l置为0
	
	//求解r数组
	top=0;
	sta[0]=0;
	For(i,1,m)//顺序枚举,利用单调不降栈线性统计每个数右边第一个小于它的数的位置(即r)
	{
		while(h[sta[top]]>h[i]) r[sta[top]]=i,--top;//右<左,则左边那个点的h会被右边限制,更新左边点的r值,退栈
		sta[++top]=i;
	}
	while(top) r[sta[top]]=m+1,--top; //一遍走完,对于单调不降栈中剩下的数中的任意一个数p,它右边所有位p+1~m的h值都比它大,故没有数能限制它,将其r置为m+1
	
	fr(i,m)ans+=(ll)(i-l[i])*(r[i]-i)*h[i];//统计答案
}

int main()
{
	fr(i,n)
	{
		fr(j,m)
		{
			while((cch=getchar())^'*'&&cch^'.');
			mp[i][j]=bool(cch^'*');
		}
	}
	
	h[0]=-Inf;
	fr(i,n)//枚举每一行,分别统计答案
	{// <核心> 对每一列求出仅受这一列的高度限制的长方形数(l[i]和r[i]以外的高度受其他数的限制(因为l[i]和r[i]都不会超过i),它管不到),即为(i-l[i])(r[i]-i)h[i],将所有列的答案相加就是以当前行为底边构造的长方形的方案数
		fr(j,m)//预处理出h(h直接在上一行的基础上进行修改,如遇*点清零,否则+1)
		{
			++h[j];
			if(!mp[i][j])h[j]=0;
		}
		workline();
	}
	cout<<ans;
	return 0;
}

T9 P2032 扫描

题目描述:

给出n,k和n个数,每次询问1~k,2~k+1,\(\dots\),n-k+1~n中的最大值。

n,k<=2e6

思路:

经典单调队列优化板子题,详见代码。

Code:

#include<bits/stdc++.h>
#define ll long long
#define fr(i,r) for(int i=1;i<=r;++i)
#define For(i,l,r) for(int i=l;i<=r;++i)
#define Rof(i,r,l) for(int i=r;i>=l;--i)
using namespace std;
const int N=2e6+5;
char cch;
ll res,zf;
inline ll rd()
{
	while((cch=getchar())<45);
	if(cch^45)res=cch^48,zf=1;
	else res=0,zf=-1;
	while((cch=getchar())>=48)res=(res*10)+(cch^48);
	return res;
}
int n=rd(),k=rd(),a[N],num[N],pos[N],h=1,t=0,i;
int main()
{
	fr(i,n)//维护一个单调递减队列
	{
		a[i]=rd();
		while(h<=t&&a[i]>=a[pos[t]])--t;//在队列不为空时,如果一个值比另一个值先出现且后出现的数值较大,那么前面那个数就为无效情况,退队
		pos[++t]=i;//pos:标记队列中每项的位置
		if(i-pos[h]>=k)++h;//队首超范围就弹出队首
		if(i>=k)cout<<a[pos[h]]<<'\n';//由于单调递减,队首的值就是最大值
	}
	return 0;
}

*T10 P2216 [HAOI2007]理想的正方形

题目描述:

思路:

Code:

#include<bits/stdc++.h>//searched
#define ll long long
#define fr(i,r) for(int i=1;i<=r;++i)
#define For(i,l,r) for(int i=l;i<=r;++i)
#define Rof(i,r,l) for(int i=r;i>=l;--i)
using namespace std;
const int N=1e3+10,Inf=0x7fffffff;
char cch;
int res,zf;
inline int rd()
{
	while((cch=getchar())<45);
	if(cch^45)res=cch^48,zf=1;
	else res=0,zf=-1;
	while((cch=getchar())>=48)res=(res*10)+(cch^48);
	return res;
}

int a=rd(),b=rd(),n=rd();
int mp[N][N];
int mx1[N][N],mi1[N][N],mx[N][N],mi[N][N];
int hmx,tmx,qmx[N],hmi,tmi,qmi[N];
int ans=Inf;

int main()
{
	fr(i,a)fr(j,b) mp[i][j]=rd();
	fr(i,a)//第一遍处理:缩列
	{
		//对每行进行单独处理:分别统计每行中每连续n个数的最大/小值
		hmx=tmx=hmi=tmi=1,qmx[1]=qmi[1]=1;//将第一位入队(!:队列qmx,qmi中存的是下标,mx1,mi1,mx,mi存的是数值)
		For(j,2,b)
		{
			//单调递减队列求长度为n的区块最大值
			while(mp[i][j]>=mp[i][qmx[tmx]]&&hmx<=tmx)--tmx;
			while(j-qmx[hmx]>=n&&hmx<=tmx)++hmx;
			qmx[++tmx]=j;
			if(j>=n) mx1[i][j-n+1]=mp[i][qmx[hmx]];//mx1[i][j]记录(i,j)~(i,j+n-1)这n个数中的最大值
			//单调递增队列求长度为n的区块最小值
			while(mp[i][j]<=mp[i][qmi[tmi]]&&hmi<=tmi)--tmi;
			while(j-qmi[hmi]>=n&&hmi<=tmi)++hmi;
			qmi[++tmi]=j;
			if(j>=n) mi1[i][j-n+1]=mp[i][qmi[hmi]];//与上边同理
		}
	}
		
	fr(j,b-n+1)//第二遍处理:缩行
	{
		//对每列进行单独处理:在对每行统计极值的基础上,分别统计每列中每连续n个数的最大/小值
		hmx=tmx=hmi=tmi=1,qmx[1]=qmi[1]=1;//将第一位入队(!:队列中存的是下标)
		For(i,2,a)
		{
			//单调递减队列求长度为n的区块最大值
			while(mx1[i][j]>=mx1[qmx[tmx]][j]&&hmx<=tmx)--tmx;
			while(i-qmx[hmx]>=n&&hmx<=tmx)++hmx;
			qmx[++tmx]=i;
			if(i>=n) mx[i-n+1][j]=mx1[qmx[hmx]][j];//mx[i][j]记录mx1(统计过列极值后的结果)中(i,j)~(i+n-1,j)这n个数中的最大值
			//单调递增队列求长度为n的区块最小值
			while(mi1[i][j]<=mi1[qmi[tmi]][j]&&hmi<=tmi)--tmi;
			while(i-qmi[hmi]>=n&&hmi<=tmi)++hmi;
			qmi[++tmi]=i;
			if(i>=n) mi[i-n+1][j]=mi1[qmi[hmi]][j];//与上边同理
		}
	}
		
	fr(i,a-n+1) fr(j,b-n+1) ans=min(ans,mx[i][j]-mi[i][j]);
	cout<<ans;
	return 0;
}
posted @ 2022-04-30 08:42  penggeng  阅读(34)  评论(0编辑  收藏  举报