[提高组互测] Day1

Poman Numbers

题目描述

点此看题

解法

以后做不出来第一题一定要打表找规律,这么辣鸡的题我空耗了两个小时

你发现每个数前面的符号是正或者负,打表发现最后一个位置的符号一定为正,倒数第二个位置的符号一定为负,其他位置的符合任填,构造方法:

因为已经知道结论了我们这里就用归纳法:

  • 如果只有一个 +,符合条件,直接退出。
  • 如果除了最后一个都是 -,那么把前面的依次拿出来即可,符合条件。
  • 否则找到倒数第二个 +,设它的位置为 (p),那么把 (|1,p+1|,|p+2,|S||) 划分,因为 (|1,p+1|) 这一段的正负序列为 ..-+,取反后变成 ..+-,正好和原序列对应,归纳到了更小的情况。

现在的问题是判断一堆二的幂次添上正负号之后能否凑出某个数,发现这是一个二选一决策问题。套路是把它转化为 (01) 决策问题,设物品大小集合为 (a_i),先把 (m+sum a_i),然后决策是否减去 (2a_i),因为物品存在倍数关系,所以把它从大到小排序之后贪心决策即可。

#include <cstdio>
#include <algorithm>
using namespace std; 
const int M = 100005;
#define int long long
int read()
{
	int x=0,f=1;char c;
	while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
	while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
	return x*f;
}
int n,m,a[M],vs[200];char s[M];
signed main()
{
	n=read();m=read();scanf("%s",s+1);
	for(int i='a';i<='z';i++)
		vs[i]=(1<<i-'a');
	m+=vs[s[n-1]]-vs[s[n]];
	for(int i=1;i<n-1;i++)
	{
		a[i]=vs[s[i]];
	    m+=a[i];
	}
	sort(a+1,a+n-1);
	for(int i=n-2;i>=1;i--)
		if(m>=2*a[i]) m-=2*a[i];
	if(m==0) puts("Yes");
	else puts("No");
}

Replace by MEX

题目描述

点此看题

解法

因为 (mex) 函数的性质,我们考虑把它变成 ([0,1,2...n-1]) 的序列。

因为操作数很少我们尽量一步到位,若当前的 (mex<n),我们把 (a[mex+1]leftarrow mex),否则我们找到任意一个 (a[i] ot=i-1),那么把 (a[i]leftarrow n),循环进行以上操作即可。

如果触发第二种操作,那么接下来会触发两次第一种操作,所以操作上界是 (frac{3n}{2}) 的。

#include <cstdio>
#include <queue>
using namespace std;
const int M = 100005;
int read()
{
	int x=0,f=1;char c;
	while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
	while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
	return x*f;
}
int T,n,m,a[M],ans[4*M],num[4*M],sum[4*M];
void ins(int i,int l,int r,int id,int f)
{
	if(l==r)
	{
		num[i]+=f;
		sum[i]=(num[i]>0);
		return ;
	}
	int mid=(l+r)>>1;
	if(mid>=id) ins(i<<1,l,mid,id,f);
	else ins(i<<1|1,mid+1,r,id,f);
	sum[i]=sum[i<<1]+sum[i<<1|1];
}
int mex(int i,int l,int r)
{
	if(l==r) return l;
	int mid=(l+r)>>1;
	if(sum[i<<1]<mid-l+1) return mex(i<<1,l,mid);
	return mex(i<<1|1,mid+1,r);
}
void op(int u)
{
	int t=mex(1,0,n);
	ins(1,0,n,a[u],-1);
	ans[++m]=u;a[u]=t;
	ins(1,0,n,a[u],1);
}
void work()
{
	n=read();m=0;
	queue<int> q;
	for(int i=1;i<=4*(n+1);i++)
		sum[i]=num[i]=0;
	for(int i=1;i<=n;i++)
	{
		a[i]=read();
		ins(1,0,n,a[i],1);
		if(a[i]!=i-1) q.push(i);
	}
	while(!q.empty())
	{
		int u=q.front();
		if(a[u]==u-1)
		{
			q.pop();
			continue;
		}
		int t=mex(1,0,n);
		if(t==n) op(u);
		else op(t+1);
	}
	printf("%d
",m);
	for(int i=1;i<=m;i++)
		printf("%d ",ans[i]);
	puts("");
}
signed main()
{
	T=read();
	while(T--) work();
}

五彩斑斓的世界

题目描述

点此看题

解法

前置技巧:如何维护一个值域中 (x) 变成 (y) 的操作,首先把所有相同的元素连起来,每个值对应到原序列的一个下标。那么如果 (y) 存在直接合并并查集,否则直接把值 (y) 对应过去。


因为本题值域很小我们考虑对于每个块维护值域,那么减法操作相当于把后面一段值域平移到前面去,如果后面更长,那么把前面移动到后面,然后打上整体减法标记即可;否则直接把后面移动到前面,就使用前置技巧维护即可。因为花多少时间值域长度就减少多少,所以均摊的复杂度是 (O(1e5))

但是本题要卡空间,我们把询问离线下来,然后对于每个块单独修改并考虑其贡献,时间复杂度 (O((n+q)sqrt n))

总结

待修的分块常常和势能法有关,我目前见到的有:设置域使得花多少时间域就减少多少、修改很少的次数时需要暴力重构,否则可以通过打标记的方式解决。

//They're the reason that I still am who I am
#include <cstdio>
#include <cstring>
#include <iostream>
#include <cmath>
using namespace std;
const int M = 1000005;
inline int read()
{
	int x=0,f=1;char c;
	while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
	while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
	return x*f;
}
inline void write(int x)
{
	if(x<=9)
	{
		putchar(x+'0');
		return ;
	}
	write(x/10);
	putchar(x%10+'0');
}
int n,m,k,Q,lz,mx,a[M],fa[M],nm[M],val[M],siz[M];
int L[M],R[M],ans[M];struct node {int op,l,r,x;}q[M];
inline int find(int x)
{
	if(x!=fa[x]) fa[x]=find(fa[x]);
	return fa[x];
}
inline void build(int w)//rebuilt the block
{
	mx=lz=0;//clear all
	for(int i=L[w];i<=R[w];i++) mx=max(mx,a[i]);
	for(int i=L[w];i<=R[w];i++)
	{
		val[i]=0;
		if(!nm[a[i]])
		{
			nm[a[i]]=i;
			val[i]=a[i];
			fa[i]=i;
		}
		else fa[i]=nm[a[i]];
		siz[a[i]]++;
	}
}
inline void change(int x,int y)//value x->y
{
	if(nm[y])
	{
		fa[nm[x]]=nm[y];
		siz[y]+=siz[x];
	}
	else
	{
		val[nm[y]=nm[x]]=y;
		siz[y]+=siz[x];
	}
	nm[x]=siz[x]=0;//good habits
}
inline void upd(int x)//sub block x
{
	if(x+lz>mx) return ;//useless
	if(x>(mx-lz)/2)//move back to front
	{
		for(int i=lz+x+1;i<=mx;i++)
			if(nm[i]) change(i,i-x);
		mx=min(mx,lz+x);//change the upper bound
	}
	else//move front to back(give tags)
	{
		for(int i=lz;i<=lz+x;i++)
			if(nm[i]) change(i,i+x);
		lz+=x;
	}
}
inline void bruteup(int w,int l,int r,int x)//brute update
{
	int ll=max(l,L[w]),rr=min(r,R[w]);
	//attention the range limit
	for(int i=L[w];i<=R[w];i++)
	{
		int t=val[find(i)];
		a[i]=t-lz;
		nm[t]=siz[t]=0;
	}
	for(int i=ll;i<=rr;i++)
		if(a[i]>x) a[i]-=x;
	build(w);
}
inline int bruteas(int w,int l,int r,int x)//brute ask
{
	int ll=max(l,L[w]),rr=min(r,R[w]),res=0;
	for(int i=ll;i<=rr;i++)
		if(val[find(i)]-lz==x) res++;
	return res;
}
void print(int w)
{
	for(int i=L[w];i<=R[w];i++)
		printf("%d ",val[find(i)]-lz);
	puts("");
}
signed main()
{
	n=read();Q=read();m=sqrt(n);
	for(int i=1;i<=n;i++)
	{
		a[i]=read();
		int w=(i-1)/m+1;
		if(!L[w]) L[w]=i;
		R[w]=i;
	}
	int t=(n-1)/m+1;
	for(int i=1;i<=Q;i++)
	{
		q[i].op=read();q[i].l=read();
		q[i].r=read();q[i].x=read();
	}
	for(int w=1;w<=t;w++)//every block
	{
		//attention the time of clearing
		for(int i=0;i<=1e5+1;i++)
			nm[i]=siz[i]=0;
		build(w);
		for(int i=1;i<=Q;i++)
		{
			int op=q[i].op,l=q[i].l,r=q[i].r,x=q[i].x;
			if(l>R[w] || L[w]>r) continue;
			if(op==1)
			{
				if(l<=L[w] && R[w]<=r) upd(x);
				else bruteup(w,l,r,x);
			}
			else
			{
				if(x+lz>mx) continue ;
				if(l<=L[w] && R[w]<=r)
					ans[i]+=siz[x+lz];
				else
					ans[i]+=bruteas(w,l,r,x);
			}
		}
	}
	for(int i=1;i<=Q;i++) if(q[i].op==2)
		write(ans[i]),puts("");
}

New Year and Binary Tree Paths

题目描述

点此看题

解法

考虑简化问题,首先考虑单链的情况。

假设有一条从点 (x) 一条长度为 (h) 一直向左的单链,那么这一条链的贡献是:

[sum_{i=0}^{h-1}2^ix=(2^h-1)x ]

从下到上的第 (iin[1,h)) 的点是向右走得到的会带来独立的贡献:(sum_{j=0}^{i-1}2^j=2^i-1)

假设 (h) 已经确定,那么起点只能是 (x=lfloorfrac{s}{2^h-1} floor),因为 (x-1) 往右一直走 (h<x) 往左一直走 (h),它们的取值范围是不重的,因为 (h)(log) 级的,所以可能的 (x) 也只有 (log) 个。


有分叉的情况就是把两个单链合起来呗,假设左边链长 (h_1),右边链长 (h_2),那么两条链都向左的和是:

[(2^{h_1}+2^{h_2}-3)x+2^{h2-1}-1 ]

同理这个 (x) 也是确定的,那么我们可以知道剩下的和 (ret)

如果中间某一步向右走,那么带来的贡献是 (2^{1}-1,2^{2}-1...2^{h_1-2}-1)(2^{1}-1,2^{2}-1...2^{h_2-2}-1),为了更好做这个 (0/1) 规划问题,我们把后面那个 (1) 去掉,枚举选出个数即可转化成二进制数位规划问题

(dp[i][j][k]) 表示前 (i) 个位选了 (j) 个数,这一位是否进位的方案数,暴力转移即可,时间复杂度 (O(log^5))

总结

对于难以计算的代价,可以考虑一种特殊情况,其他情况都从这种情况修改得来(怎么感觉我写过这句话?!)

#include <cstdio>
#include <cstring>
#include <iostream>
using namespace std;
const int M = 105;
#define int long long
int read()
{
	int x=0,f=1;char c;
	while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
	while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
	return x*f;
}
int n,m,ans,pw[M],dp[2][M][2];
int cal(int n,int a,int b,int t)
{
	int nw=0;
	memset(dp[nw],0,sizeof dp[nw]);
	dp[0][0][0]=1;
	for(int i=1;i<=m;i++)//attention the upper bound 
	{
		if((1ll<<i)>n) break;
		int d=(n>>i)&1;nw^=1;
		memset(dp[nw],0,sizeof dp[nw]);
		for(int j=0;j<=t;j++)
		for(int k=0;k<2;k++) if(dp[nw^1][j][k])
		for(int x=0;x<2;x++) if(!x || i<a)
		for(int y=0;y<2;y++) if(!y || i<b)
		if((x+y+k)%2==d)
			dp[nw][x+y+j][(x+y+k)/2]+=dp[nw^1][j][k];
	}
	return dp[nw][t][0];
}
signed main()
{
	n=read();pw[0]=1;
	while(pw[m]<=n) {pw[m+1]=pw[m]*2;m++;}
	for(int i=1;i<=m;i++) for(int j=1;j<=m;j++)
	{
		if(n-pw[j-1]+1<0) continue;
		int x=(n-pw[j-1]+1)/(pw[i]+pw[j]-3);
		if(x<=0) continue;
		int ret=(n-pw[j-1]+1)-x*(pw[i]+pw[j]-3);
		for(int k=0;k<i+j-1;k++) if(!((ret+k)&1))
			ans+=cal(ret+k,i-1,j-1,k);
	}
	printf("%lld
",ans);
}
原文地址:https://www.cnblogs.com/C202044zxy/p/15398834.html