UNR #6

总结

比较丢人,感觉是最近打过最差的大型比赛,对标 \(\tt NOI\) 的话可能就是只比银牌线多一点。

第一天:\(100+10+40=150\)

第二天:\(20+10+20=50\)

真是什么都想不到,只能打最基础的暴力,而且打暴力还挂了四十分。挂分的原因是数据分治是最基本的暴力写挂了没发现,应该是比较容易检查出来的。首要任务还应该是保证不挂分,这样只打暴力都能混进前 \(100\) 左右

做到不挂分还是挺简单的,各种检查方法相结合吧。不仅要对拍,还应该肉眼反复检查,测试各种边界情况,当然这些都是老生常谈了,用心的话肯定可以做好。

至于想不想得到正解这件事,现在我也没有办法勉强了,但是 \(\tt NOI\) 的时候谁又说得准呢?

心态好才是最重要的,反正我也是运气选手,大家都比我高有什么关系呢?(嬉皮笑脸

话说要提前去参赛地点了,现在找到一个稳定的球场才是关键

机器人表演

题目描述

点此看题

解法

为了方便描述,下文称新加入的 \(01\) 为左括号和右括号。

考虑对于确定的末状态如何判断合法性。采用贪心匹配的思想,即优先匹配原串,如果原串无法匹配再用括号。这样会遇到一个问题:如果出现了和原串无法匹配的 \(1\),但是场上已经没有未匹配的左括号,这个 \(1\) 就会失配。

本来这个 \(1\) 可以不必失配,解决方法就是把前面一个和原串匹配的 \(0\) 替换为左括号。所以我们暴力撤回已匹配原串的一段后缀,把他们全部替换成左右括号,直接获得一个未匹配的左括号,就可以停止撤回。

上面的方法是方便计数的,因为按照这样的贪心匹配方法,得到的一定是原串匹配位置子序列的字典序最小的合法解。所以建出自动机之后直接 \(dp\) 计数,设 \(f[i][j][k]\) 表示考虑到第 \(i\) 位,场上没有匹配的左括号有 \(j\) 个,原串已经匹配到了 \(k\) 的方案数。

题外话:写完正解之后翻了翻官方题解,发现直接暴力 \(dp\)\(dp\) 都有 \(45\) 分,我是脑瘫。

#include <cstdio>
#include <cstring>
#include <iostream>
using namespace std;
const int M = 305;
const int MOD = 998244353;
#define mp make_pair
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,f[2][M][M];char s[M];
pair<int,int> t[M][M][2];
void add(int &x,int y) {x=(x+y)%MOD;}
signed main()
{
	n=read();m=read();scanf("%s",s+1);
	for(int i=0;i<=m;i++) for(int j=0;j<=n;j++)
	{
		t[i][j][0]=(j<n && s[j+1]=='0')?mp(i,j+1):mp(i+1,j);
		if(j<n && s[j+1]=='1') t[i][j][1]=mp(i,j+1);
		else if(i) t[i][j][1]=mp(i-1,j);
		else
		{
			int p=j,x=0;
			while(p && x>=0) x+=s[p--]=='0'?-1:1;
			t[i][j][1]=x<0?mp(0,p):mp(-1,-1);
		}
	}
	f[0][0][0]=1;int w=0;
	for(int i=1;i<=(n+2*m);i++)
	{
		w^=1;memset(f[w],0,sizeof f[w]);
		for(int j=0;j<=m;j++) for(int k=0;k<=n;k++)
			for(int x=0;x<2;x++)
			{
				int u=t[j][k][x].first,v=t[j][k][x].second;
				if(~u) add(f[w][u][v],f[w^1][j][k]);
			}
	}
	printf("%d\n",f[w][0][n]);
}

稳健型选手

题目描述

点此看题

解法

首先考虑对于一个序列如何贪心,考虑先手选出来的子序列必须满足:把先手标记为 \(-1\),后手标记为 \(1\),那么任意前缀的权值和 \(\geq -1\),从后往前考虑这个限制,我们维护一个堆,每到一个奇数位置就把堆顶取出来,选取它然后把他从堆中删掉。这个做法在必须选取的时候才选取了当前最大值,所以是正确的。

使用回滚莫队维护这个过程,时间复杂度 \(O(q\sqrt n\log n)\)

考虑猫树分治,每次处理过中点的询问。首先对左边和右边分别求出最优的选择,设左边的选取集合是 \(S\),右边的未选取集合是 \(T\),那么合并带来的效果是:取消一些 \(S\) 的选择,转而去选择 \(T\) 中的元素。

取消的一定是 \(S\) 的一段后缀,选择的一定是 \(T\) 的一段前缀。可以用权值线段树维护 \(S\)\(T\),合并时在线段树上二分,找到一个分界线使得左侧的 \(S\) 个数等于右侧的 \(T\) 个数。

要保留多个版本的线段树,我们使用主席树维护。分治时处理出主席树后就可以轻松回答询问,时间复杂度 \(O(q\log^2 n)\)

关键细节:对于奇偶性的处理,我们按 \(l\)\(mid\) 距离的奇偶性讨论,如果是偶数,那么就两个两个的选;如果是奇数,我们就选取 \(mid\) 而不选取 \(mid+1\),这个可以通过合并调整回来。

#include <cstdio>
#include <vector>
#include <iostream>
#include <queue>
using namespace std;
const int M = 200005;
const int N = 30*M;
#define int long long
const int up = 1e9;
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],L[M],R[M],ans[M],rt[M];
int cnt,ls[N],rs[N],num[N],s[N];
vector<int> h[M];priority_queue<int> q;
void ins(int &x,int l,int r,int p)
{
	int y=x;x=++cnt;num[x]=num[y];
	ls[x]=ls[y];rs[x]=rs[y];s[x]=s[y];
	num[x]++;s[x]+=p;
	if(l==r) return ;
	int mid=(l+r)>>1;
	if(mid>=p) ins(ls[x],l,mid,p);
	else ins(rs[x],mid+1,r,p);
}
int ask(int x,int y,int l,int r,int u,int v)
{
	if(l==r) return (u-v)*l;
	int mid=(l+r)>>1;
	int u1=u+num[ls[x]],v1=v+num[rs[y]];
	if(u1>v1) return ask(ls[x],ls[y],l,mid,u,v1)+s[rs[y]];
	if(u1<v1) return ask(rs[x],rs[y],mid+1,r,u1,v)-s[ls[x]];
	return s[rs[y]]-s[ls[x]];
}
void cdq(int l,int r,vector<int> &v)
{
	if(!v.size()) return ;
	if(l==r)
	{
		for(int x:v) ans[x]=a[l];
		return ;
	}
	vector<int> vl,vr;
	int mid=(l+r)>>1;
	for(int x:v)
	{
		if(R[x]<=mid) vl.push_back(x);
		else if(L[x]>mid) vr.push_back(x);
		else h[R[x]].push_back(x);
	}
	for(int o=0;o<2;o++)
	{
		for(int i=1;i<=cnt;i++)
			ls[i]=rs[i]=s[i]=num[i]=0;
		cnt=0;while(!q.empty()) q.pop();
		for(int i=mid;i>=l;i--)
		{
			rt[i]=(i!=mid)?rt[i+1]:0;
			q.push(a[i]);
			if((mid-i^o)&1)
				ins(rt[i],1,up,q.top()),q.pop();
		}
		while(!q.empty()) q.pop();
		int cur=0;
		for(int i=mid+1;i<=r;i++)
		{
			rt[i]=(i!=mid+1)?rt[i-1]:0;
			cur+=a[i];q.push(-a[i]);
			if((i-mid-1^o)&1)
				ins(rt[i],1,up,-q.top()),
				cur+=q.top(),q.pop();
			for(int x:h[i]) if((mid-L[x]^o)&1)
				ans[x]=cur+s[rt[L[x]]]+
				ask(rt[L[x]],rt[R[x]],1,up,0,0);
		}
	}
	for(int i=l;i<=r;i++) h[i].clear();
	cdq(l,mid,vl);cdq(mid+1,r,vr);
}
signed main()
{
	n=read();m=read();
	for(int i=1;i<=n;i++) a[i]=read();
	vector<int> v;
	for(int i=1;i<=m;i++)
		L[i]=read(),R[i]=read(),v.push_back(i);
	cdq(1,n,v);
	for(int i=1;i<=m;i++)
		printf("%lld\n",ans[i]);
}

小火车

题目描述

点此看题

解法

充分利用 \(2^n>p\) 的条件,可以做出如下问题转化:给每个物品加权 \(0/1\),问是否存在两种权值和相同的方案。这是因为只要把这两种加权方案按位相减,就可以得到原问题的加权方案。

根据鸽笼原理一定有解,也就是一定存在一个笼子放了至少两个加权方案,我们的目标是找到这个笼子。

考虑经典方法(我真的感觉这个方法在哪里见过,但是又想不起来了),假设现在已经确定目标笼子在 \([l,r]\) 中,设 \(s_{l,r}\) 表示权值 \([l,r]\) 的笼子含有的方案总数。找到区间的中点 \(mid\),判断 \(s_{l,mid}>mid-l+1\) 是否成立,如果成立那么左半边一定有目标笼子,如果不成立那么右半边一定有目标笼子,分治下去即可。

问题是快速计算 \(s_{l,r}\),可以折半搜索预处理并且提前排序,这样每次检测双指针就行了,时间复杂度 \(O(2^{n/2}\log p)\)

#include <cstdio>
#include <vector>
#include <iostream>
#include <algorithm>
using namespace std;
#define int long long
#define pii pair<int,int>
#define fi first
#define se second
#define mp make_pair
const int M = 1<<20;
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,p,x,y,k,a[45],b[3],lg[M];pii f[M],g[M];
void add(int d,int l,int r)
{
	while(l<r && k<2)
		b[k++]=f[d].se+x*g[l].se,l++;
}
int calc(int s1,int s2)
{
	int l=0,r=0,s=0;k=0;
	for(int i=x-1;i>=0;i--)
	{
		while(l<y && f[i].fi+g[l].fi<s1) l++;
		while(r<y && f[i].fi+g[r].fi<=s2) r++;
		s+=r-l;add(i,l,r);
	}
	s1+=p;s2+=p;
	for(int i=x-1;i>=0;i--)
	{
		while(l<y && f[i].fi+g[l].fi<s1) l++;
		while(r<y && f[i].fi+g[r].fi<=s2) r++;
		s+=r-l;add(i,l,r);
	}
	return s;
}
signed main()
{
	n=read();m=n/2;p=read();
	x=(1<<m);y=(1<<n-m);lg[0]=-1;
	for(int i=0;i<n;i++) a[i]=read();
	for(int i=1;i<x;i++)
	{
		lg[i]=lg[i>>1]+1;f[i].se=i;
		f[i].fi=(f[i&(i-1)].fi+a[lg[i&(-i)]])%p;
	}
	for(int i=1;i<y;i++)
	{
		lg[i]=lg[i>>1]+1;g[i].se=i;
		g[i].fi=(g[i&(i-1)].fi+a[lg[i&(-i)]+m])%p;
	}
	sort(f,f+x);sort(g,g+y);
	int l=0,r=p-1;
	while(l<r)
	{
		int mid=(l+r)>>1;
		if(calc(l,mid)>mid-l+1) r=mid;
		else l=mid+1;
	}
	calc(l,l);
	for(int i=0;i<n;i++)
		printf("%lld ",(b[0]>>i&1)-(b[1]>>i&1));
}

神隐

题目描述

点此看题

解法

考虑二进制分组,对于每个数位,把包含这一位的边拿出来问一遍,再把不包含这一位的边拿出来问一遍。设数位总数是 \(m\),那么两个点直接有边相连当且仅当恰好 \(m\) 次出现在同一个连通块中。这是因为对于不直接相连的点,他们的简单路径上一定存在两条边,使得某个数位分别是 \(0\)\(1\),这样在这个数位上是无论如何也连通不了的。

询问次数大概是 \(2\log n\),利用经典方法优化。我们对所有边重编号,使得每个边恰好有一半的数位是 \(1\),这样询问时只需要把这一位包含 \(1\) 的边拿出来问一遍。比如对于 \(n\leq 2000\),数位大小钦定为 \(w=14\),如果连通次数为 \(7\) 就判定为这两个点直接有边相连,正确性来源于 \({14\choose 7}>2000\)\(n\leq 2^{17}\) 同理,钦定为 \(w=20\) 即可)

这样询问次数满足了条件,但是时间复杂度 \(O(n^2\log n)\),只能拿到 \(50\) 分。

考虑加速构建树的过程,我们从叶子往上确定树。如果一个点在 \(>2\) 的连通块的次数恰好为 \(\frac{w}{2}\),那么它肯定是叶子。考虑求出它的父亲,父亲一定是 \(\frac{w}{2}\) 个连通块的交,由于两个连通块的交一定包含至少一个连通块的顶部,考虑求交的过程,父亲一定是这些连通块最深的顶部。

因为删叶子的过程中已经提供了一个深度的顺序,所以在某个点作为顶部删除的时候,和它同在一个连通块内的点,如果还没有确定父亲,那么父亲就是当前删除的点(相当于在删除父亲的时候再确定父亲)

删除一个叶子时,只需要把它从包含它的连通块中删掉即可。类似一个拓扑排序的过程,时间复杂度 \(O(n\log n)\)

#include "tree.h"
#include <bits/stdc++.h>
using namespace std;
const int M = (1<<17)+5;
const int N = 17*M;
#define vc vector
int m,k,c,id[N],cnt[M],fa[M],vis[M],s[N];
vc<int> vec,d[N],in[M];
vc<pair<int,int>> ans;
//
vc<pair<int,int>> solve(int n)
{
	m=n<=2000?14:20;
	for(int i=0;i<(1<<m);i++)
		if(__builtin_popcount(i)==m/2) id[k++]=i;
	vec.resize(n-1);
	for(int i=0;i<m;i++)
	{
		for(int j=0;j+1<n;j++)
			vec[j]=id[j]>>i&1;
		vc<vc<int>> t=query(vec);
		for(auto x:t)
		{
			d[++c]=x;s[c]=x.size();
			for(int y:x)
				in[y].push_back(c),cnt[y]+=s[c]>1;
		}
	}
	queue<int> q;
	for(int i=0;i<n;i++)
		if(cnt[i]==m/2) q.push(i);
	while(!q.empty())
	{
		int u=q.front();q.pop();
		for(int i:in[u])
		{
			if(s[i]==1) for(int x:d[i])
				if(u^x && !vis[x]) vis[x]=1,fa[x]=u;
			s[i]--;
			if(s[i]==1) for(int x:d[i])
				if((--cnt[x])==m/2) q.push(x);
		}
	}
	for(int i=0;i<n;i++)
		if(vis[i]) ans.push_back(make_pair(i,fa[i]));
	return ans;
}
posted @ 2022-08-07 21:56  C202044zxy  阅读(315)  评论(8编辑  收藏  举报