[提高组互测] 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]\not=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\n",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\) 一直向左的单链,那么这一条链的贡献是:
从下到上的第 \(i\in[1,h)\) 的点是向右走得到的会带来独立的贡献:\(\sum_{j=0}^{i-1}2^j=2^i-1\)
假设 \(h\) 已经确定,那么起点只能是 \(x=\lfloor\frac{s}{2^h-1}\rfloor\),因为 \(x-1\) 往右一直走 \(h<x\) 往左一直走 \(h\),它们的取值范围是不重的,因为 \(h\) 是 \(\log\) 级的,所以可能的 \(x\) 也只有 \(\log\) 个。
有分叉的情况就是把两个单链合起来呗,假设左边链长 \(h_1\),右边链长 \(h_2\),那么两条链都向左的和是:
同理这个 \(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\n",ans);
}