2019牛客暑期多校训练营(第三场)(B、D、F、G、H、J)

心得

组合计数、计算几何、模拟、dp,堪称我的四大垃圾领域

B.Crazy Binary String(思维题)

给你一个长度不超过N(N<=1e5)的01串,

问你最长01个数相等的子串,最长01个数相等的子序列,分别是多长

 

01子串,把1视为单点+1,0视为单点-1,作一遍前缀和,

那么,当前点r可以在上一个前缀和与之相同的点l-1之后续上一段[l,r],这一段[l,r]和为0也就是01数量相等

01子序列,显然能取到2*min(0,1)的个数

#include
using namespace std;
#define pb push_back
const int N=1e5;
char s[N+10];
int len,n0,n1;
int ans;
int last[N*2+10],now[N*2+10],sum[N+10];
//0 -1 1 -1 -1 1 -1 -1 1
//0 -1 0 -1 -2 -1 -2 -3 -2
//sum[r]==sum[l-1][l,r]==0 r-(l-1)+1==r-l
int main()
{
    while(~scanf("%d",&len))
    {
        scanf("%s",s+1);
        n0=0;n1=0;
        for(int i=1;i<=len;++i)
        {
            if(s[i]=='0')n0++,sum[i]=sum[i-1]-1;
            if(s[i]=='1')n1++,sum[i]=sum[i-1]+1;
            last[N+sum[i]]=-1;
            now[N+sum[i]]=0;
        }
        last[N]=0;now[N]=0;
        ans=0;
        for(int i=1;i<=len;++i)
        {
            if(~last[N+sum[i]])
            {
                now[N+sum[i]]+=i-(last[N+sum[i]]);
                ans=max(ans,now[N+sum[i]]);
            }
            last[N+sum[i]]=i;
        }
        printf("%d %d\n",ans,2*min(n0,n1));
    }
    return 0;
}

H.Magic Line(计算几何)

给你N(N为偶数且N<=1e3)个不同的点(xi,yi),|xi|,|yi|<=1e3

要求你输出两个整点,使之确定的直线,将平面划成两部分后,一边恰有一半

 

考虑按y增序排,y相同按x增序,也就是处理二维偏序的排序方式,

如果过n/2和n/2+1两个点的中点,画一条斜率为负,但近似水平的线,是可以将上下剖开的

但这个中点可能不是整点,所以直接用n/2和n/2+1两个点平移,

一个左移1e8单位,一个右移1e8单位,从而将直线拉平

但n/2和n/2+1两个点可能位于同一水平线上,这个时候,

左边的点再上移1单位,右边的点下移1单位,就可以使原来的两个点不重合了

#include
using namespace std;
const int N=1e3+5;
const int off=1e8;
int t,n;
struct node
{
	int x,y;
}e[N],c,d;
bool operator<(node a,node b)
{
	return a.y

J.LRU management(模拟/双向链表)

模拟题,让你实现LRU替换算法的原理,

即如果不在块中就加进去,如果在,就更新它的最后访问时间

如果块满,就删掉块内最早一个访问的元素,

还有,询问按插入顺序的前驱和后继的值

 

模拟,先hash,注意,把字符串压成10位整数的ans不能从0乘,

不然区分不了0和00这种前导0字符串,应从res=1乘

我太菜了,也许只有我犯这个错误

 

双向链表,map维护是对应的第几个块,相当于id号,

而链表维护这个块在什么位置,更新位置时,将其按索引访问,把前驱后继一改,挪到最后

由于按时间戳维护的这条双向链表,在最前面的就是被轮到前面去的,

开始建两个虚节点head和tail放INF,便于查前驱和后继不用特判NULL

 

小学期,当时缓存置换算法用的LRU,那个时候仔细想了想,想用Splay实现

后来觉得Java实现起来太复杂,就用的两个map,今天下午搞了一发c++Splay然后血wa,当然最后调过了

Splay放进去的也是时间戳的标号,i从1到q的编号,更新时取出last删掉,然后改成i再塞回去

Splay也得放一个-INF和INF,便于查前驱和后继

双向链表+map

#include
using namespace std;
typedef long long ll;
const int maxn=5e5+10; 
const int INF=0x3f3f3f3f;
int T,q,m,op,v,last,tot,cnt;
//带索引的双向链表 
//删除时只删除头结点的后一个的位置
//复杂度O(nlogn) 
char s[15];
unordered_mappos;
struct node
{
	node* pre;
	node* nex;
	int v; 
	ll rk;
}head,tail,arr[maxn];
void cut()
{
	node* x=head.nex;
	node* xx=x->nex;
	xx->pre=&head;
	head.nex=xx;
	pos.erase(x->rk);
	cnt--;
}
void add(node *x)
{
	node* xx=tail.pre;
	xx->nex=x;
	x->pre=xx;
	x->nex=&tail;
	tail.pre=x; 
	cnt++;//代表链表中有几个值 
	if(cnt>m)cut();//有两个虚节点 故超过m+2才删 
}
void del(int x)
{
	node* xx=arr[x].pre;
	node* xxx=arr[x].nex;
	xx->nex=xxx;
	xxx->pre=xx;
	cnt--;
}
void debug()
{
	node* A = &head;
    while(A != NULL)
    {
        printf("->%d ", A->v);
        A = A->nex;
    }
    printf("\n");
}
int main()
{
	scanf("%d",&T);
	while(T--)
	{
		scanf("%d%d",&q,&m);
		cnt=0;tot=0;
		pos.clear();
		head.pre=NULL;head.nex=&tail;head.v=INF;
		tail.pre=&head;tail.nex=NULL;tail.v=INF;
		for(int i=1;i<=q;++i)
		{
			scanf("%d%s%d",&op,s,&v);
			ll rk=1;
			int len=strlen(s);
			for(int j=0;jv;
						if(res==INF)puts("Invalid");
						else printf("%d\n",res);
					}
					else if(v==1)
					{
						int res=(arr[p].nex)->v;
						if(res==INF)puts("Invalid");
						else printf("%d\n",res);
					}
				}
			}
		} 
	}
	return 0;
}
/*
1
8 3
0 0101010 1
0 0101011 2
1 0101010 1
0 1100000 3
0 0101011 -1
0 1111111 4
1 0101011 -1
1 0101010 0
*/

Splay+map

#include
using namespace std;
typedef long long ll;
const int INF=2147483647;
const int maxn=5e5+10;
inline int read()
{
    register int x=0,t=1;
    register char ch=getchar();
    while(ch!='-'&&(ch<'0'||ch>'9'))ch=getchar();
    if(ch=='-'){t=-1;ch=getchar();}
    while(ch>='0'&&ch<='9'){x=x*10+ch-48;ch=getchar();}
    return x*t;
}
int root,tot;
struct query
{
    int op;
    char s[11];
    ll Hash;//hash值
    int rk;//rank
    int v;
}e[maxn];
ll hs[maxn],num;
struct Node
{
    int ch[2];//左右儿子
    int val;//值
    int ff;//父节点
    int size;//子树大小
    int cnt;//数字的数量
}t[maxn];
void pushup(int u)//下放操作
{
    t[u].size=t[t[u].ch[0]].size+t[t[u].ch[1]].size+t[u].cnt;
    //当前子树的大小是左子树大小加上右子树大小当前当前节点个数
}
void rotate(int x)//旋转操作
{
    int y=t[x].ff;//y是x的父节点
    int z=t[y].ff;//z是y的父节点
    int k=(t[y].ch[1]==x);//x是y的左儿子(0)还是右儿子(1)
    t[z].ch[t[z].ch[1]==y]=x;//把x旋转为z的儿子
    t[x].ff=z;//x的父亲更新为z
    t[y].ch[k]=t[x].ch[k^1];//把x的儿子给y
    t[t[x].ch[k^1]].ff=y;//更新父节点
    t[x].ch[k^1]=y;//y变为x的
    t[y].ff=x;//y的父亲更新为x
    pushup(y);pushup(x);//更新子节点数量
}
void splay(int x,int goal)//旋转操作,将x旋转为goal的儿子
{
    while(t[x].ff!=goal)
    {
        int y=t[x].ff;//x的父亲节点
        int z=t[y].ff;//x的祖父节点
        if(z!=goal)//如果z不是goal
            (t[y].ch[0]==x)^(t[z].ch[0]==y)?rotate(x):rotate(y);
            //如果x和y同为左儿子或者右儿子先旋转y
            //如果x和y不同为左儿子或者右儿子先旋转x
            //如果不双旋的话,旋转完成之后树的结构不会变化
        rotate(x);//再次旋转x,将x旋转到z的位置
    }
    if(goal==0)//如果目标位置是0,则是将x旋转到根节点的位置
        root=x;//更新根节点
}
void insert(int x)//插入x
{
    int u=root,ff=0;//当前位置u,u的父节点ff
    while(u&&t[u].val!=x)//当u存在并且没有移动到当前的值
    {
        ff=u;//向下u的儿子,父节点变为u
        u=t[u].ch[x>t[u].val];//大于当前位置则向右找,否则向左找
    }
    if(u)//存在这个值的位置
        t[u].cnt++;//增加一个数
    else//不存在这个数字,要新建一个节点来存放
    {
        u=++tot;//新节点的位置
        if(ff)//如果父节点非根
            t[ff].ch[x>t[ff].val]=u;
        t[u].ch[0]=t[u].ch[1]=0;//不存在儿子
        t[tot].ff=ff;//父节点
        t[tot].val=x;//值
        t[tot].cnt=1;//数量
        t[tot].size=1;//大小
    }
    splay(u,0);//把当前位置移到根,保证结构的平衡
}
bool find(int x)//查找x的位置,并将其旋转到根节点
{
    int u=root;
    if(!u)return false;//树空
    while(t[u].ch[x>t[u].val]&&x!=t[u].val)//当存在儿子并且当前位置的值不等于x
        u=t[u].ch[x>t[u].val];//跳转到儿子,查找x的父节点
    splay(u,0);//把当前位置旋转到根节点
    return t[u].val==x;
}
int Next(int x,int f)//查找x的前驱(0)或者后继(1) 返回的是节点号
{
    find(x);
    int u=root;//根节点,此时x的父节点(存在的话)就是根节点
    if(t[u].val>x&&f)return u;//如果当前节点的值大于x并且要查找的是后继
    if(t[u].val1)//如果超过一个
    {
        t[del].cnt--;//直接减少一个
        splay(del,0);//旋转
    }
    else
        t[next].ch[0]=0;//这个节点直接丢掉(不存在了)
}
int kth(int x)//查找排名为x的数 从小到大
{
    int u=root;//当前根节点
    if(t[u].sizet[y].size+t[u].cnt)
        //如果排名比左儿子的大小和当前节点的数量要大
        {
            x-=t[y].size+t[u].cnt;//数量减少
            u=t[u].ch[1];//那么当前排名的数一定在右儿子上找
        }
        else//否则的话在当前节点或者左儿子上查找
            if(t[y].size>=x)//左儿子的节点数足够
                u=y;//在左儿子上继续找
            else//否则就是在当前根节点上
                return t[u].val;
    }
}
int T,q,m,op,rk,v,last;
int cao[maxn],vis[maxn];
int main()
{
    scanf("%d",&T);
    while(T--)
    {
        root=0;
        tot=0;//清空splay
        insert(INF);
        insert(-INF);
        scanf("%d%d",&q,&m);
        for(int i=1;i<=q;++i)
        {
            scanf("%d%s%d",&e[i].op,e[i].s,&e[i].v);
            ll res=1;
            int len=strlen(e[i].s);
            for(int j=0;j

F.Planting Trees(单调队列)

N*N(N<=500)的矩阵,求一个最大矩阵面积,使得矩阵内的最大值-最小值<=M(0<=M<=1e5)

矩阵内的元素1<=ai<=1e5,保证\sum N^{3}<25e7,所以可以用O(N^3)做法

 

枚举上限行top,枚举下限行down,对于固定的top,

down向下枚举过程中,最大值只增不减,最小值只减不增,

有继承的性质,那只需要O(n)求出左右扩充的距离即可,

这里参考hdu3530,枚举右端点,可以单调队列求出左端点到什么位置

维护的是一个开口向左的喇叭口形状,最大值单调递减队列,最小值单调递增队列,

队列首就是这一段区间的最大值和最小值,只需要保证队列首的差值在[0,M]间即可,

每次剔除不合法位置时,从左边开始剔

#include
#include
#include
using namespace std;
const int maxn=505;
typedef long long ll;
int T,n,m;
int a[maxn][maxn],b[maxn][maxn];
int q1[maxn],head1,rear1;//最大值递减单调队列 
int q2[maxn],head2,rear2;//最小值递增单调队列 
int L[maxn],R[maxn];
int mx[maxn],mn[maxn];
ll ans;
ll solve(int p[maxn][maxn])
{
	ll res=0;
	for(int top=1;top<=n;++top)//枚举悬线上限行 
	{
		for(int i=1;i<=n;++i)
		mx[i]=-1e9,mn[i]=1e9;//每一条悬线的最大值
		for(int down=top;down<=n;++down)//枚举悬线下限行 
		{
			int now=1;
			head1=rear1=0;
			head2=rear2=0;
			for(int i=1;i<=n;++i)
			{
				mx[i]=max(mx[i],p[down][i]);
				mn[i]=min(mn[i],p[down][i]);
			}
			for(int i=1;i<=n;++i)
			{
				while(head1mn[i])rear2--;
				q1[rear1++]=i;
				q2[rear2++]=i;
				while(head1m)
				{
					if(q1[head1]=0)
				res=max(res,1ll*(i-now+1)*(down-top+1));
			}
		}
	}
	return res;
}
int main()
{
	scanf("%d",&T);
	while(T--)
	{
		scanf("%d%d",&n,&m);
		for(int i=1;i<=n;++i)
		for(int j=1;j<=n;++j)
		scanf("%d",&a[i][j]);
		printf("%lld\n",solve(a)); 
	} 
	return 0;
}

D.Big Integer(数论)

一个数列,第一项是1,第二项是11,第n项是n个1的字符串表示的数,

现在,令A(q)代表第q项,现在给定n,m,素数p,均<=1e9,

求有多少项i(1<=i<=n),j(1<=j<=m),满足A(i^{j})\equiv 0(mod\ p)

 

p=2或5时,11111显然mod 2或mod 5不能为0,

去掉了这两种情况之后,就可以保证底数和模数互质了,从而欧拉定理一波

9e9*9e9,一开始没用快速乘,WA自闭了槽

2019牛客暑期多校训练营(第三场)(B、D、F、G、H、J)_第1张图片

2019牛客暑期多校训练营(第三场)(B、D、F、G、H、J)_第2张图片

#include
using namespace std;
typedef long long ll;
const int maxn=1e5;
bool ok[maxn];
ll prime[maxn],cnt;
ll fac[40],num[40];//循环节的素因子 及其出现次数 
int t;
ll p,n,m;
void sieve()
{
	for(ll i=2;i=maxn)break;
			ok[i*prime[j]]=1;
			if(i%prime[j]==0)break; 
		}
	}
}
ll mul(ll x,ll n,ll mod)
{
	ll res=0;
	for(;n;n>>=1,x=(x+x)%mod)
	if(n&1)res=(res+x)%mod;
	return res; 
}
ll modpow(ll x,ll n,ll mod)//mod最大9p 9e9 
{
	ll res=1;
	for(;n;n>>=1,x=mul(x,x,mod))//9e9*9e9 注意快速乘 
	if(n&1)res=mul(res,x,mod);//
	return res; 
}
ll calphi(ll x)//x 9e9
{
	ll ans=x;
	for(int i=0;ix)break;
		if(x%prime[i]==0)
		{
			ans=ans/prime[i]*(prime[i]-1);
			while(x%prime[i]==0)x/=prime[i];
		}
	}
	if(x>1)ans=ans/x*(x-1);
	return ans;
} 
int main()
{
	sieve();
	scanf("%d",&t);
	while(t--)
	{
		scanf("%lld%lld%lld",&p,&n,&m);
		if(p==2||p==5){puts("0");continue;}//1111%2不可能为0 1111%5不可能为0
		//10^n==1(mod 9p) 循环节必为phi(9*p)的约数
		ll mx=calphi(9*p);//最大循环节9p 
		ll d=mx;//循环节的值 
		for(ll i=1;i*i<=mx;++i)
		{
			if(mx%i==0)
			{
				if(id)break;
			if(d%prime[i]==0)
			{
				fac[++tot]=prime[i];
				num[tot]=1;d/=prime[i];//省去初始化 
				while(d%prime[i]==0)num[tot]++,d/=prime[i];
			}	
		}
		if(d>1)fac[++tot]=d,num[tot]=1;
		ll res=0;
		for(ll j=1;j<=min(30ll,m);++j)
		{
			ll g=1;
			for(int k=1;k<=tot;++k)
			{
				ll up=(num[k]+j-1)/j;
				g*=modpow(fac[k],up,1e10);//g<9p 所以相当于没模  
			}
			res=res+n/g;
			if(j==30)res=res+n/g*(m-30);
		}
		printf("%lld\n",res);
	}
	return 0;
}
/*
1
998244353 247394239 247924723 开快速乘!9e9*9e9 
*/

G.Removing Stones(分治)

N(N<=3e5)堆石子,第i堆个数1到1e9,每次Mary可以选择一个区间[L,R]

在这些堆石子上玩以下游戏:

①如果石子总和为奇数,从最少堆删去一个,然后进行步骤②,否则直接步骤②

②每次选择两个非空堆,各取一个石子

若最后石子可取完,则称Mary胜,否则败,

问有多少[l,r]符合条件

 

考虑对于每一个最大值mx,包含它的区间其余数的和得大于等于mx

也就是区间的数的总和,至少为2*mx,

ST预处理最大值位置,便于RMQ,预处理前缀和和后缀和

分治时,统计两个端点同区间和跨区间的贡献,

将当前[l,r]的最大值当做必取的值,在短的区间里枚举端点,在另一个区间里二分

由于后缀和是倒序的,所以实际上从后往前第pos的值得用从前往后第n+1-pos的位置推导

代码比较简洁,然而调下标相当繁琐

#include
using namespace std;
const int maxn=3e5+10;
typedef long long ll;
int t,n;
int dp[maxn][20];
int dep[maxn];
ll pre[maxn],suf[maxn];
void ST(int tot)
{
	for(int i=1;i<=tot;++i)
	dp[i][0]=i;//存的是最小值的下标 
	for(int len=1;(1<dep[dp[l+(1<<(len-1))][len-1]])dp[l][len]=dp[l][len-1];
			else dp[l][len]=dp[l+(1<<(len-1))][len-1];
		}
	}
}
int RMQ(int l,int r)//返回最大值下标 
{
	int len=log(r-l+1)/log(2);
	if(dep[dp[l][len]]>dep[dp[r-(1<=r)return 0;
	int pos=RMQ(l,r);//枚举最大值点 
	ll ans=0;
	//处理短的一半 保证复杂度为log 
	if(pos-l=2*dep[pos]的最左位置 
		ans+=r-(lower_bound(pre+pos,pre+r+1,pre[i-1]+2*dep[pos])-pre)+1; 
	}
	else
	{
		for(int i=pos;i<=r;++i)
		ans+=(n+1-(lower_bound(suf+(n+1-pos),suf+(n+1-l)+1,suf[(n+1-(i+1))]+2*dep[pos])-suf))-l+1;
	} 
	return ans+cdq(l,pos-1)+cdq(pos+1,r); 
}
int main()
{
	scanf("%d",&t);
	while(t--)
	{
		scanf("%d",&n);
		for(int i=1;i<=n;++i)
		scanf("%d",&dep[i]);
		for(int i=1;i<=n;++i)
		pre[i]=pre[i-1]+dep[i];
		for(int i=1;i<=n;++i)//便于正向二分 逆序处理 
		suf[i]=suf[i-1]+dep[n+1-i];
		ST(n);
		printf("%lld\n",cdq(1,n));
	}
	return 0;
}

 

你可能感兴趣的:(牛客多校)