基础算法

基础算法

二分答案

当答案满足一定的单调性(如对于一个值成立,小于它的均成立,大于它的可能不成立,因此就要找大于它的;如果它不成立,大于它的均不成立,小于它的可能成立,因此就要找小于它的。直到找到一个值小于它成立(包括自己),大于它不成立,这个就是最值(常常也是答案))。

本质上是通过二分方法不停枚举,检验答案(检验比正面做简单)。需要满足:可检验和二分性

例题

  1. 跳石头
ll a[M],L,n,m; 
ll ans;
bool check(ll x)
{
	ll i,j;
	ll now=0,s=0;
	for(i=1;i<=n+1;i++)
	{
		if(a[i]-a[now]<x) s++;
		else now=i;
	}
	if(s>m) return false;
	return true;
}
int main()
{
	ll i,j;
	L=read();
	n=read(),m=read();
	for(i=1;i<=n;i++){
		a[i]=read();
	}
	a[n+1]=L;
	ll l=1,r=L;
	while(l<=r)
	{
		ll mid=(l+r)>>1;
		if(check(mid)){
			ans=mid;
			l=mid+1;
		}
		else {
			r=mid-1;
		}
 	}
	printf("%lld",ans);
	return 0;
} 

因为这道题目的是要求最小距离的最大值,因此每次找到最小距离,还应该试试有没有更大的,即 l = m i d + 1 l=mid+1 l=mid+1

每个二分答案都有check函数。

这里检验连续两个点之间的距离。

这里是如果两个点距离大于在检验的最小距离(x),再检验下一个点对。

如果两个点距离小于,记录 s u m + + sum++ sum++,(sum用来删点,即 s u m ≤ m sum\leq m summ),如果有两个点距离小于x,则必有一个点多余,删去一个,因为我们是往右边查询,删去右边的点显然比删去左边的好,因为更右边的点距离左边的点距离肯定大于距离右边的点。(如果是最后一个区间,删去右端点相当于删去左端点)。

程序具体实现只用记录现在检查到哪个点,如果右端点删掉,那么这个左指针就不变,只变右指针。

  1. 奇怪的函数
ll a[M],L,n,m; 
ll ans;
bool check(ll x)
{
	ll i,j;
	if(x*log10(x)>=(double)n-1) return true;
	else return false;
}
int main()
{
	ll i,j;
	n=read();
	ll l=1,r=n+10;
	while(l<=r)
	{
		ll mid=(l+r)>>1;
		if(check(mid)){
			ans=mid;
			r=mid-1;
		}
		else {
			l=mid+1;
		}
 	}
	printf("%lld",ans);
	return 0;
} 

x x ≥ 1 0 n − 1 x^x\geq 10^{n-1} xx10n1很简单知道可以二分答案,但 x x x^x xx太大

但我们知道,有指数可考虑取对数。 x lg ⁡ x ≥ n − 1 x\lg x\geq n-1 xlgxn1变简单了许多

附:如果不是以10为底数的log,可用 log ⁡ a b = lg ⁡ b lg ⁡ a \log_{a}b=\frac {\lg b}{\lg a} logab=lgalgb表示

  1. 通往奥格瑞玛的道路
struct edge{
	ll x,y,nx,cost;
}e[M<<1];
ll n,m,b,a[N];
ll head[N],cnt;
ll cost[N],ans;
bool vis[N];
void add_edge(ll x,ll y,ll c)
{
	e[++cnt].x=x; e[cnt].y=y;
	e[cnt].nx=head[x]; head[x]=cnt;
	e[cnt].cost=c; 
}
struct cmp{
	bool operator () (ll x,ll y){
		return cost[x]>cost[y];
	}
};

bool check(ll k)
{
	if(k<max(a[1],a[n])) return false;
	priority_queue<ll,vector<ll>,cmp> q;
	ll i,j;
	vis[1]=true;
	for(i=head[1];i;i=e[i].nx)
	{
		ll y=e[i].y,cc=e[i].cost;
		if(cc<=b){
			cost[y]=cc;
			q.push(y);
		}
	}
	
	while(q.size())
	{
		ll s=q.top(); q.pop();
		if(vis[s]) continue;
		vis[s]=true;
		if(cost[s]>b) break;
		if(s==n){
			return true;
		}
		
		for(i=head[s];i;i=e[i].nx)
		{
			ll x=e[i].y,cc=e[i].cost;
			if(vis[x]||cc+cost[s]>b||a[x]>k) continue;
			if(cost[x]==0||cost[x]>cc+cost[s]){
				cost[x]=cc+cost[s];
				q.push(x);
			}
		}		
	}
	return false;
}
int main()
{
	ll i,j;
	n=read(),m=read(),b=read();
	for(i=1;i<=n;i++){
		a[i]=read();
		ans=max(a[i],ans);
	}
	for(i=1;i<=m;i++)
	{
		ll x=read(),y=read(),cc=read();
		if(x==y) continue;
		add_edge(x,y,cc);
		add_edge(y,x,cc);
	}
	ll l=1,r=ans;
	bool f=false;
	while(l<=r)
	{
		clr(vis);
		clr(cost);
		ll mid=(l+r)>>1;
		if(check(mid)){
			r=mid-1;
			ans=mid;
			f=true;
		}
		else{
			l=mid+1;
		}
	}
	if(!f) printf("AFK");
	else 
	printf("%lld",ans);
	return 0;
} 

这道题求的最大费用不是和!!!是路径上的max,因此用二分答案很合适。

在check时只需要用Dijkstra找伤害最少的路劲(同时最大费用不能超过k)

  1. [USACO11MAR]布朗尼切片Brownie Slicing
ll s[N][N],n,m,a[N][N],A,B,ans=inf;
bool check(ll x)
{
	ll i,j;
	ll sum=0,now=0;
	for(i=1;i<=n;i++)
	{
		ll he=0,num=0;
		for(j=1;j<=m;j++)
		{
			he+=s[i][j]-s[i][j-1]-s[now][j]+s[now][j-1];
			if(he>=x)
			{
				he=0;
				num++;
			}	
		}
		if(num>=B)
		{
			sum++;
			now=i;		
		}		
	}
	if(sum>=A)	return true;
	return false;	
}
int main()
{
	ll i,j;
	n=read(),m=read(),A=read(),B=read();
	for(i=1;i<=n;i++){
		for(j=1;j<=m;j++){
			a[i][j]=read();
			s[i][j]=s[i-1][j]+s[i][j-1]-s[i-1][j-1]+a[i][j];
			ans=min(a[i][j],ans);
		}
	}
	
	ll l=ans,r=s[n][m];
	while(l<=r)
	{
		ll mid=(l+r)>>1;
		if(check(mid))
		{
			ans=mid;
			l=mid+1;
		}
		else r=mid-1;
	}
	
	printf("%lld",ans);
	return 0;
} 

二分性很明显,但check有难度。先预处理二维前缀和。

在检验中,设now为之前切的在哪一行,然后扫列,每当一列连续的大于x,就 n u m + + num++ num++,并 h e = 0 he=0 he=0(因为其和已经大于检验值,不需要在往里面加入值),一直扫下去,如果 n u m ≥ b num\geq b numb,说明可以且 b − 1 b-1 b1刀, s u m + + , n o w = i sum++,now=i sum++,now=i(原因和之前一样);如果 n u m < b num<b num<b,说明还需要加入下一行。

最后判断 s u m sum sum a a a大小即可

  1. [SHOI2015]自动刷题机
ll a[M],L,n,m,k; 
ll ans1,ans2;
int check(ll x)
{
	ll i,j;
	ll s=0,res=0;
	for(i=1;i<=n;i++)
	{
		s=max(0ll,s+a[i]);
		if(s>=x) {s=0;	res++;}
	}
	if(res==k) return 0;
	if(res>k) return -1;
	if(res<k) return 1;
}
int main()
{
	ll i,j;
	n=read(),k=read();
	for(i=1;i<=n;i++){
		a[i]=read();
	}
	bool f=false;
	ll l=1,r=999999999999999;
	while(l<=r)
	{
		ll mid=(l+r)>>1;
		ll c=check(mid);
		if(c==0){
			ans1=mid;
			r=mid-1;
			f=true;
		}
		else if(c>0)
			r=mid-1;
		else 
			l=mid+1;
 	}
 	
 	l=1,r=999999999999999;
	while(l<=r)
	{
		ll mid=(l+r)>>1;
		ll c=check(mid);
		if(c==0){
			ans2=mid;
			l=mid+1;
		}
		else if(c>0)
			r=mid-1;
		else 
			l=mid+1;
 	}
 	
 	if(!f) printf("-1");
 	else
	printf("%lld %lld",ans1,ans2);
	return 0;
} 

这道题判断相等,因此二分比较有意思。大于说明值小了 l = m i d + 1 l=mid+1 l=mid+1,小于说明值大了 r = m i d − 1 r=mid-1 r=mid1

贪心

寻找当前某个性质的局部最优解,进而成为全局最优解。

例题

  1. [POI2006]MET-Subway

  1. [JSOI2010]缓存交换(cache)
struct node{
	ll id,wei;
	bool operator < (const node &a) const{
		return wei>a.wei;
	}
}a[M];
priority_queue<ll> q;
ll ans,n,m,rank[M<<1],nxt[M],last[M];
bool vis[M];
int main()
{
	ll i,j;
	n=read(),m=read();
	for(i=1;i<=n;i++){
		a[i].wei=read();
		a[i].id=i;
	}
	ll sum=0;
	sort(a+1,a+n+1);
	ll cnt=0;
	a[0].wei=-19;
	for(i=1;i<=n;i++)	
	{
		if(a[i].wei!=a[i-1].wei){
			rank[a[i].id]=++cnt;
		}
		else rank[a[i].id]=cnt;
	}
	
	for(i=1;i<=n;i++)
	{
		nxt[last[rank[i]]]=i;
		last[rank[i]]=i;
	}
	for(i=1;i<=n;i++){nxt[last[rank[i]]]=n+i;rank[n+i]=rank[i];}//!!!最后一次出现
	for(i=1;i<=n;i++)
	{
		if(vis[rank[i]]){q.push(nxt[i]); continue;}
		if(sum<m)
		{
			sum++;
			ans++;
			q.push(nxt[i]);
			vis[rank[i]]=true;
		}
		else
		{
			ll y=rank[q.top()],x=rank[i];
			q.pop();
			vis[y]=false;
			vis[x]=true;
			q.push(nxt[i]);
			ans++;
		}
	}
	
	
	printf("%lld",ans);
	return 0;
} 

运算符重载

本题是贪心,找最晚出现的(不是出现最少的)。

举例说明:1 2 3(cache中),还有4…3…2…1…(其中均表示其最早出现),对于4,假如换1,那么直到1出现之前,和换掉其他的比肯定不会坏,最后再换一次。如果换3 变成1 2 4,到3时,如果换4,变成 1 2 3,显然没有最开始换1好,换2(或1)变成1 3 4 就相当于最开始换2(或1),显然也不好。

  1. 国王游戏
struct man{
	ll a,b;
}a[1010];
ll n,lena,lens,lend;
int ans[N],s[N],d[N];
void com();
void mul(ll x);
void div(ll x);
bool cmp1(man x,man y){return x.a*x.b<y.a*y.b;}
int main()
{
	ll i,j;
	n=read();
	a[0].a=read(),a[0].b=read();
	for(i=1;i<=n;i++){
		a[i].a=read(),a[i].b=read();
	}
	sort(a+1,a+n+1,cmp1);
	ll x=a[0].a;
	while(x)
	{
		s[++lens]=x%10;
		x/=10;
	}
	for(i=1;i<=n;i++)
	{
		div(a[i].b);
		com();
		mul(a[i].a);
	}
	
	for(i=1;i<=lena;i++)
	printf("%d",ans[i]);
	return 0;
}
void com()
{
	ll i,j;
	if(lena<lend)
	{
		lena=lend;
		for(i=1;i<=lend;i++)
		ans[i]=d[i];
	}
	else if(lena==lend){
		bool f=false;
		for(i=1;i<=lend;i++)
		{
			if(ans[i]>d[i]) break;
			if(ans[i]<d[i]) {f=true; break;}
		}
		
		if(f)
		{
			for(i=1;i<=lend;i++)
			ans[i]=d[i];			
		}
	}
}
void mul(ll x)
{
	ll i,j,c=0;
	for(i=1;i<=lens;i++)
	{
		s[i]*=x;
		s[i]+=c;
		c=s[i]/10;
		s[i]=s[i]-c*10;
	}
	while(c)
	{
		s[++lens]=c%10;
		c/=10;
	}
}
void div(ll x)
{
	ll ss=0,i,j;
	lend=0;
	bool f=false;
	for(i=lens;i>=1;i--)
	{
		ss*=10;
		ss+=s[i];
		if(ss>=x)
		{
			d[++lend]=ss/x;
			ss%=x;
			f=true;	
		}	
		else if(f)
		{
			d[++lend]=0;
		}
	}	
}

贪心找 l [ i ] ∗ r [ i ] l[i]*r[i] l[i]r[i]最小的。证明:

看相邻的两个,他们俩的顺序不会影响他们前后的值。

设前面乘积为 x x x,一位大臣 l 1 , r 1 l_1,r_1 l1,r1,另二位大臣 l 2 , r 2 l_2,r_2 l2,r2

两种排法得到的值分别是 s 1 = m a x ( l 1 ∗ x r 1 , l 1 ∗ l 2 ∗ x r 2 ) s_1=max(\frac {l_1*x}{r_1},\frac{l_1*l_2*x}{r_2}) s1=max(r1l1x,r2l1l2x)

s 2 = m a x ( l 2 ∗ x r 2 , l 1 ∗ l 2 ∗ x r 1 ) s_2=max(\frac {l_2*x}{r_2},\frac{l_1*l_2*x}{r_1}) s2=max(r2l2x,r1l1l2x)

注意到 l 1 ∗ l 2 ∗ x r 1 > l 1 ∗ x r 1 , l 1 ∗ l 2 ∗ x r 2 > l 2 ∗ x r 2 \frac{l_1*l_2*x}{r_1}>\frac {l_1*x}{r_1},\frac{l_1*l_2*x}{r_2}>\frac {l_2*x}{r_2} r1l1l2x>r1l1x,r2l1l2x>r2l2x

不妨另 s 2 ≥ s 1 s_2\geq s_1 s2s1(另一种情况一样)

显然 s 2 = l 1 ∗ l 2 ∗ x r 1 ≥ l 1 ∗ l 2 ∗ x r 2 s_2=\frac{l_1*l_2*x}{r_1}\geq \frac{l_1*l_2*x}{r_2} s2=r1l1l2xr2l1l2x

得到 l 2 r 2 ≥ l 1 r 1 l_2r_2\geq l_1r_1 l2r2l1r1

这道题要用高精,高精比较,高精×低精,高精除低精。(出发为简单,用小端存储)。

  1. 皇后游戏
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值