海亮寄 7.12
前言
业精于勤荒于嬉,行成于思毁于随
正文(模拟赛)
卦象:吉
(然而模拟赛进程并不吉利,分数还没有破百,还不如去年国庆的水平呢)
感受:难受啊,深深的无力感,就有一种啥都会但又啥都不会的无力感。一开始扫了一圈所有题目,T1 口胡了一个广搜做法,T2 疑似先拆绝对值再拆 \(\min\),T3 可能会静态问题,搞一个容斥,动态问题观测到不带修,以及数据范围很灵性,可能是根号相关(具体来说是莫队?),T4 大概扫了一眼直接放弃,着手准备暴力分数。本来规划的是一个小时搓 T1,一个半到两个小时搓 T3,然后看剩余时间搓 T2,T4 暴力可以作为换脑子的广告环节,反正 10min 内肯定能写完。规划非常清晰,但却没有正确评估自己的能力。变故出在了 T1 上——完全没想到一个普通的广搜写了两个半小时,然后只写了 30pts 的数据点,难受。最后无奈选择打掉 T2、T3、T4 的暴力。回过头来写 T1 的
n=3的数据,未果,最后获得 75/400 的优秀成绩,接近北中倒数第一咯!也没有什么可以总结的,T2 想的 CDQ 复杂度赛后说会被卡,T3 想到正解但也没有时间了,T4 赛后还是不会。但 T3 的分数是应该拿到的,所以只能加训一下广搜,决定择日重写高效进阶的广搜部分
(已破防,勿扰)
T1
人口普查,然而自己菜到了不是人
观测到一些密码锁具体数值并不重要,重要的只有差值,所以可以简单做
隔壁的 dalao 说可以先爆搜是 \(+/- 1\) 情况,然后控制一下上下界 \(+ / -\) 的操作次数上界,最后转化为 这个题目
讲题时还有人提出,可以区间的 \(+/- 1\) 可以转化为差分数组上的操作,然后最小化操作次数直接就上点什么最优化问题的经典策略即可
(感觉自己的广搜还是实现的太劣了,除了单次查询 \(O(1)\) 没有任何优势)
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N=1e4+5,M=5e5+5;
int n,Q,head[N],tot,a[5],b[5],c[5];
string x,y;
struct Edge{int to,nxt;}e[M];
int dis[N],ans[N][N];bool vis[N];
inline void add(int u,int v){
e[++tot]={v,head[u]};head[u]=tot;
return;
}
inline void make2(){
for(int i=0;i<=99;i++){
int a=i%10,b=i/10%10;
int v1=(a+1)%10,v2=(b+1)%10;
add(i,b*10+v1),add(i,v2*10+a),add(i,v2*10+v1);
v1=(a+9)%10,v2=(b+9)%10;
add(i,b*10+v1),add(i,v2*10+a),add(i,v2*10+v1);
}
return;
}
inline void bfs2(int S){
for(int i=0;i<=99;i++)dis[i]=0,vis[i]=false;
queue<int> q;q.push(S);dis[S]=0;vis[S]=true;
while(!q.empty()){
int u=q.front();q.pop();
// cerr<<"Zyx "<<u<<' '<<head[u]<<endl;
for(int i=head[u];~i;i=e[i].nxt){
int v=e[i].to;
if(vis[v])continue;
// cerr<<u<<' '<<v<<endl;
// cerr<<"-----------"<<endl;
dis[v]=dis[u]+1;vis[v]=true;q.push(v);
}
}
return;
}
inline void make3(){
for(int i=0;i<=999;i++){
int a=i%10,b=i/10%10,c=i/100%10;
int v1=(a+1)%10,v2=(b+1)%10,v3=(c+1)%10;
add(i,c*100+b*10+v1),add(i,c*100+v2*10+a),add(i,v3*100+b*10+a);
add(i,c*100+v2*10+v1),add(i,v3*100+v2*10+a),add(i,v3*100+v2*10+v1);
v1=(a+9)%10,v2=(b+9)%10,v3=(c+9)%10;
add(i,c*100+b*10+v1),add(i,c*100+v2*10+a),add(i,v3*100+b*10+a);
add(i,c*100+v2*10+v1),add(i,v3*100+v2*10+a),add(i,v3*100+v2*10+v1);
}
return;
}
inline void bfs3(int S){
for(int i=0;i<=999;i++)dis[i]=0,vis[i]=false;
queue<int> q;q.push(S);dis[S]=0;vis[S]=true;
while(!q.empty()){
int u=q.front();q.pop();
// cerr<<"Zyx "<<u<<' '<<head[u]<<endl;
for(int i=head[u];~i;i=e[i].nxt){
int v=e[i].to;
if(vis[v])continue;
// cerr<<u<<' '<<v<<endl;
// cerr<<"-----------"<<endl;
dis[v]=dis[u]+1;vis[v]=true;q.push(v);
}
}
return;
}
inline void make4(){
for(int i=0;i<=9999;i++){
int a=i%10,b=i/10%10,c=i/100%10,d=i/1000%10;
int v1=(a+1)%10,v2=(b+1)%10,v3=(c+1)%10,v4=(d+1)%10;
add(i,d*1000+c*100+b*10+v1),add(i,d*1000+c*100+v2*10+a);
add(i,d*1000+v3*100+b*10+a),add(i,v4*1000+c*100+b*10+a);
add(i,d*1000+c*100+v2*10+v1),add(i,d*1000+v3*100+v2*10+a),add(i,v4*1000+v3*100+b*10+a);
add(i,d*1000+v3*100+v2*10+v1),add(i,v4*1000+v3*100+v2*10+a);
add(i,v4*1000+v3*100+v2*10+v1);
v1=(a+9)%10,v2=(b+9)%10,v3=(c+9)%10,v4=(d+9)%10;
add(i,d*1000+c*100+b*10+v1),add(i,d*1000+c*100+v2*10+a);
add(i,d*1000+v3*100+b*10+a),add(i,v4*1000+c*100+b*10+a);
add(i,d*1000+c*100+v2*10+v1),add(i,d*1000+v3*100+v2*10+a),add(i,v4*1000+v3*100+b*10+a);
add(i,d*1000+v3*100+v2*10+v1),add(i,v4*1000+v3*100+v2*10+a);
add(i,v4*1000+v3*100+v2*10+v1);
}
return;
}
inline void bfs4(int S){
for(int i=0;i<=9999;i++)dis[i]=0,vis[i]=false;
queue<int> q;q.push(S);dis[S]=0;vis[S]=true;
while(!q.empty()){
int u=q.front();q.pop();
// cerr<<"Zyx "<<u<<' '<<head[u]<<endl;
for(int i=head[u];~i;i=e[i].nxt){
int v=e[i].to;
if(vis[v])continue;
// cerr<<u<<' '<<v<<endl;
// cerr<<"-----------"<<endl;
dis[v]=dis[u]+1;vis[v]=true;q.push(v);
}
}
return;
}
int main(){
freopen("lock.in","r",stdin);
freopen("lock.out","w",stdout);
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin>>n>>Q;
memset(head,-1,sizeof(head));
if(n==1){
while(Q--){
int x,y;cin>>x>>y;
int ans=abs(x-y);
cout<<min(ans,10-ans)<<'\n';
}
return 0;
}
if(n==2){
make2();
bfs2(0);
}
else if(n==3){
make3();
bfs3(0);
}
else if(n==4){
make4();
bfs4(0);
}
while(Q--){
string x,y;cin>>x>>y;x=' '+x,y=' '+y;
for(int i=1;i<=n;i++)a[n-i+1]=x[i]-'0';
for(int i=1;i<=n;i++)b[n-i+1]=y[i]-'0';
for(int i=1;i<=n;i++)c[i]=(b[i]+10-a[i])%10;
// for(int i=1;i<=n;i++)cerr<<a[i]<<' '<<b[i]<<' '<<c[i]<<endl;
// cerr<<endl<<"-------------------"<<endl;
int t=0;
for(int i=1;i<=n;i++){
if(i==1)t+=c[i];
else if(i==2)t+=(c[i]*10);
else if(i==3)t+=(c[i]*100);
else if(i==4)t+=(c[i]*1000);
}
// cerr<<t<<endl;
// cerr<<"The end!"<<endl;
cout<<dis[t]<<'\n';
}
return 0;
}
T2
用老师的话说,就是做的题太少
注意到两个求和号内的式子形如最值套绝对值,容易联想到切比雪夫距离,所以先将 \(\min\) 改成 \(\max\)
然后切比雪夫距离转曼哈顿距离,直接做做完了
(听上去有点蒙,所以需要推式子)
具体地,有:
然后就转化为做四遍
排序,扫描右端点,每次加前缀和即可!
点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=5e5+5;
int n,a[N],b[N],c[N],d[N],sum[N];
inline int cal(int x[]){
sort(x+1,x+n+1);
for(int i=1;i<=n;i++)sum[i]=0;
for(int i=1;i<=n;i++)sum[i]=sum[i-1]+x[i];
int ans=0;
for(int i=1;i<=n;i++)ans+=(x[i]*i-sum[i]);
return ans;
}
signed main(){
freopen("dist.in","r",stdin);
freopen("dist.out","w",stdout);
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin>>n;
for(int i=1;i<=n;i++)cin>>a[i];
for(int i=1;i<=n;i++)cin>>b[i];
for(int i=1;i<=n;i++)c[i]=a[i]+b[i];
for(int i=1;i<=n;i++)d[i]=a[i]-b[i];
cerr<<cal(a)<<' '<<cal(b)<<' '<<cal(c)<<' '<<cal(d)<<endl;
cout<<cal(a)*2+cal(b)*2-cal(c)-cal(d)<<'\n';
return 0;
}
T3
静态问题好做吗?非常好做,观测到这种恰好 \(x\) 个不相等的东西,直接考虑大力容斥
记 \(F(k)\) 表示钦定至少 \(k\) 个位置满足 \(P_i = a_i\) 的方案数,有:
然后求 \(F(k)\),组合数学、大力容斥计算不了的计数题就交给 DP
记 \(f_{i,j}\) 表示只考虑值域 \([1,i]\) 且恰好存在 \(j\) 个位置满足 \(P_x = a_x\) 的方案数,转移方程形如:
其中 \(cnt_i\) 表示查询区间中值为 \(i\) 的元素个数
容易发现单次查询 \(O(n^2)\)
然后这个东西咋优化呢,一个很经典的观测即不带修且数据范围灵性,可以根号,具体地,可以莫队!
复杂度上没啥问题,大概是一个 \(O(q \times n \sqrt{n})\) 的东西
问题在于如何插入贡献以及删除贡献
回到那个转移方程上来,显然可以把第一维度丢掉,即
我们发现,插入一个元素相当于给 \(cnt\) 自增一,删除一个元素相当于给 \(cnt\) 自减一
那么,就有了一个比较 naive 的做法
对于 add 操作来说,先把 \(cnt\) 的部分取出来,然后自增一,最后再丢进去
对于 del 操作来说,先把 \(cnt\) 的部分取出来,然后自减一,最后再丢进去
需要注意的是,如果出现了新的 cnt 或者消失了一个 cnt,要对 \(f\) 数组更新
点击查看代码
// 背包撤销那一套正序枚举或者倒序枚举不会啊!!!
// 菜完了菜完了菜完了菜完了啊!!!
// 谁懂老师代码出来的那一瞬间的救赎感!!!
// 初极狭,才通人。复行数十步,__________
// 原来可以直接把原有的 DP 数组给拷贝下来,更新 cnt 后重新算!!!
// 真是菜完了,这么经典的操作居然不会!!!
// 有的人活着,他已经死了,有的人死了,他的确死了
// 有的人活着,他已经不想活了,有的人死了,他死得透透的了
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=3e3+5,MOD=1e9+7;
int n,Q,a[N],siz,fac[N];
int len,cnt[N],f[N],tmp[N],ans[N];
struct que{
int l,r,blk,id;
friend bool operator < (que s,que t){
if(s.blk!=t.blk)return s.blk<t.blk;
return ((s.blk&1)?(s.r<t.r):(s.r>t.r));
}
}q[N];
inline void ADD(int &x,int y){x=(x+y)%MOD;return;}
inline void init(){
fac[0]=1;
for(int i=1;i<=n;i++)fac[i]=fac[i-1]*i%MOD;
return;
}
inline void add(int k){
int x=cnt[a[k]];cnt[a[k]]++;
for(int i=1;i<=len;i++)tmp[i]=(f[i]+x*tmp[i-1]%MOD)%MOD;
x++;
for(int i=1;i<=len;i++)f[i]=(tmp[i]-x*tmp[i-1]%MOD+MOD)%MOD;
if(x==1){len++;f[len]=(MOD-x*tmp[len-1]%MOD)%MOD;}
return;
}
inline void del(int k){
int x=cnt[a[k]];cnt[a[k]]--;
for(int i=1;i<=len;i++)tmp[i]=(f[i]+x*tmp[i-1]%MOD)%MOD;
x--;
for(int i=1;i<=len;i++)f[i]=(tmp[i]-x*tmp[i-1]%MOD+MOD)%MOD;
if(x==0)len--;
return;
}
inline int cal(){
// cerr<<len<<endl;
// for(int i=0;i<=len;i++)cerr<<f[i]<<' ';
// cerr<<endl;
// for(int i=0;i<=len;i++)cerr<<fac[i]<<' ';
// cerr<<endl;
// cerr<<"--------------------------"<<endl;
int res=0;
for(int i=0;i<=len;i++)ADD(res,f[i]*fac[n-i]%MOD);
return res;
}
signed main(){
freopen("neq.in","r",stdin);
freopen("neq.out","w",stdout);
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin>>n>>Q;siz=sqrt(n);
init();
for(int i=1;i<=n;i++)cin>>a[i],a[i]++;
for(int i=1;i<=Q;i++){
int l,r;cin>>l>>r;l++;
q[i]={l,r,l/siz,i};
}
sort(q+1,q+Q+1);
f[0]=tmp[0]=1;int l=1,r=0;
for(int i=1;i<=Q;i++){
int ql=q[i].l,qr=q[i].r,idx=q[i].id;
while(l>ql)add(--l);
while(r<qr)add(++r);
while(l<ql)del(l++);
while(r>qr)del(r--);
ans[idx]=cal();
}
for(int i=1;i<=Q;i++)cout<<ans[i]<<'\n';
return 0;
}
每日犯唐:重计算不等于多递推一轮
(代码中 len 表示 \(f_{i,j}\) 第二维度上限)
T4
不会不会怎么办?只能明天研读一下 FSZ 奆的代码了……
Upd. 晚上开窍了!会 T4 了!
观测部分分,可以发现能够搞一个 DP,状态设计大概是 \(f_{i,c,len}\) 表示枚举前缀 \([1,i]\),代价为 \(c\),包含下标 \(i\) 的最后一段连续段为 \(len\)
首先先处理一个问题,即 \(c\) 的上限是 \(T\),所以可能会炸。然而我们发现 x 数组大小不大,且 \(n\) 也不大,暴力枚举一圈后发现有效的状态数不多。所以可以先把所有可能的代价记录下来离散化,这样状态设计就没有问题了
然后扫一遍 DP,刷表转移更容易理解,比如——
其中 \(\to\) 符号是广义的,表示动词“贡献”
然后捏,这一套东西可能只能通过部分测试点。究其原因,还是 \(c\) 太大了!
所以,考虑折半
容易发现,这个 DP 正着做和倒着做是没有差异的,所以我们在序列上取一个 \(mid\),然后先正着扫一遍 \(1 \to mid\) 的 DP,再倒着扫一遍 \(r \to (mid+1)\) 的 DP,最后考虑将两个 DP 数组进行合并
此时我们大概能想到的是什么
-
所需要的 DP 的答案应该是 \(f_{mid,???,0/1/2/3/4}\)
-
首先枚举左边的 \(len\),并枚举 \(???\)
-
需要从右边找到一个最优解
-
同理,枚举右边的 \(len\),需要保证中间拼合的段长度合法
-
找一个最大的且保证代价不超过 \(T\) 的右边的 DP 值
-
最大的是好弄的,可以维护一个 \(c\) 上的后缀最大值
-
那么右边的 DP 值就是最大的合法的 \(c\),对应的后缀最大 DP 值
随便维护即可,时间复杂度 \(O(\text{能过})\)
(代码咕咕咕)
小结
破防,加训走起!
后记
世界孤立我任它奚落
完结撒花!

浙公网安备 33010602011771号