模拟赛总结
2024.2.6 【寒假集训】20240206测试
T1 珠子
看来是关于双指针的神秘东西。
T2 数组
这个题,我没考虑到的是要保留全部的 \(x,y\) 操作信息,以及上一次 \(A\) 操作的时间等等。
代码(参考 lcy):
#include<bits/stdc++.h>
#define int long long
using namespace std;
int n,m;
int t1[500001],t2[500001];
int lst[50000100];
int pos,dx;
signed main()
{
cin>>n>>m;
int su=(n+1)*n/2;
pos=0,t1[0]=1,t2[0]=0;
// for(int i=1;i<=n;i++)lst[i]=i;
for(register int i=1;i<=m;++i)
{
char c=getchar();
int x,y;cin>>x>>y;
t1[i]=x,t2[i]=y;
if(c=='A')
{
pos=i,dx=0;
printf("%lld\n",x*su+n*y);
}
else
{
if(lst[t1[i]]<=pos)
{
dx+=t2[i]-(t1[i]*t1[pos]+t2[pos]);
lst[t1[i]]=i;
}
else
{
int last=lst[t1[i]];
dx-=t2[last]-(t1[i]*t1[pos]+t2[pos]);
dx+=t2[i]-(t1[i]*t1[pos]+t2[pos]);
lst[t1[i]]=i;
}
printf("%lld\n",t1[pos]*su+t2[pos]*n+dx);
}
}
return 0;
}
T3 幸运区间
题意:在序列 \(a\) 中,求出所有子序列 \(b\) 中, \(\gcd(b)=1\) 的个数。
(\(\sim\) 表示从 \(x\) 到 \(y\) 的所有元素)
考虑画出一个表示所有子序列的 \(\gcd\) 的三角形矩阵:
1
1 1
1 1 1
1 1 1 1
1 1 1 1 1
1 1 1 1 1 1
1 1 1 1 1 1 1
1 1 1 1 1 1 1 1
5 1 4 1 3 1 2 1 1
5 5 4 4 3 3 2 2 1 1
我们可以看到,从最下面开始,对于每个左端点确定的区间,只要在右端点为 \(r\) 的时候 \(\gcd=1\) ,那么从\([l,r]\) 到 \([l,n]\),所有区间的 \(\gcd=1\)。
有一个定理:只要一个序列的子序列 \(\gcd=1\),那么这个序列 \(\gcd=1\)。
所以,我们需要实现两个东西:
-
查找区间 \([l,r]\) 的 \(\gcd\)。
-
找到对于一个 \(l\in [1,n]\),\(r\) 最小并且 \(\gcd=1\) 的子序列,统计答案
ans+=n-r+1
。
对于查找,由上面的定理可知, \(\gcd\) 具有传递性,因此我们可以构建一棵线段树来实现此操作。
tr[now].gcd=__gcd(tr[lid].gcd,tr[rid].gcd);
对于找到一个 \(l\)、满足条件的最小的 \(r\),我们考虑直接 for 循环去找(区间伸缩)。
#include<bits/stdc++.h>
using namespace std;
#define int long long
int n,m;
int a[200100];
class emw{
public:
#define lid now<<1
#define rid now<<1|1
void build(int now,int l,int r)
{
if(l==r){
tr[now].g=a[l];return ;
}
int mid=(l+r)>>1;
build(lid,l,mid),build(rid,mid+1,r);
tr[now].g=__gcd(tr[lid].g,tr[rid].g);
}
int getgcd(int now,int l,int r,int x,int y)
{
if(x<=l&&r<=y)return tr[now].g;
int mid=(l+r)>>1;
int res=0;
if(x<=mid) res=__gcd(res,getgcd(lid,l,mid,x,y));
if(y>mid) res=__gcd(res,getgcd(rid,mid+1,r,x,y));
return res;
}
private:
struct segTree{
int g;
}tr[200100<<2];
}st;
int ans;
signed main()
{
cin>>n;
for(int i=1;i<=n;i++)cin>>a[i];
st.build(1,1,n);
int l=1,r=1;
for(l=1;l<=n;l++)
{
r=max(l,r);
while(l<=r&&r<=n)
{
if(st.getgcd(1,1,n,l,r)==1)
{
break;
}
r++;
}
ans+=(n-r+1);
}
cout<<ans;
return 0;
}
T4 找不同
官方题解的 RMQ 我没理解,但是理解了题解区某位大佬的线性 DP,深受震撼。
思路是这样的:
- 定义 \(dp[i]\) 表示以 \(i\) 为右端点、最长的、使得区间内没有重复出现单词的左端点。
形式化地,定义 \(dp[i]\) 表示 \(dp[i]=j,\forall x,y \in [j,i],a[x] \neq a[y]\),并且对于所有 \(j\) 符合,要求 \(j=dp[i]\) 最小。
- 状态转移时,我们会用
map<string,int>last
记录当前字符串 \(a[i]\) 上一次出现的位置(下标),显然 \(dp[i]\) 有可能从 \(last[a[i]]+1\) 转移而来,并且 \(dp[i]\) 的值不会小于等于 \(last[a[i]]\)。
-
那么还能怎么转移?
-
我们在转移 \(i-1\) 之后,已经知道了 \(dp[i-1]\) 的值,意思就是我们知道了以 \(i-1\) 为右端点的合法区间的最大长度。如果在这个区间 \([j_i-1,i-1]\) 中没有 \(a[i]\) ,那么 \(dp[i]\) 一定等于 \(dp[i-1]\)。
-
如果 \(last[a[i]]>dp[i-1]\) ,就是在上一个合法区间内有一个 \(a[i]\),那么 \(dp[i]=last[a[i]]+1\)。
-
综上所述,我们可以得到状态转移方程:
- 对于每个询问 \(x,y\),我们只需要判断 \(dp[y]\le x\) 即可得到答案。
意思是如果 \(dp[y]\le x\),那么 \(x\) 在 \(y\) 对应的合法区间内,一定满足要求,输出 YES
;
否则输出 NO
。
#include<bits/stdc++.h>
using namespace std;
int n,m;
string a[100101];
int dp[100101];
map<string,int>last;
int main()
{
cin>>n;
for(int i=1;i<=n;i++)
{
cin>>a[i];
dp[i]=max(dp[i-1],last[a[i]]+1);
last[a[i]]=i;
}
cin>>m;
while(m--)
{
int x,y;cin>>x>>y;
if(dp[y]<=x)
{
cout<<"YES\n";
}
else cout<<"NO\n";
}
}
2024.2.18 【寒假集训】20240218测试
T1 家庭作业
太可恶了!!!
没想到被离散化背刺了。
这个题思路很简单,只需要找出 \(a\) 和 \(b\) 的所有质因数是什么及其个数。
我考场上用的桶,想着省事开了个 map
,结果炸了。
考完试改了十分钟,把一个桶改成两个数组,加上 \(cnt\) 就过了。
真令人忍俊不禁。
for(int i=1;i<=cnta;i++)
{
int kk=findb(ax[i]);
if(kk>0)
{
// cout<<min(aa[i],bb[i])<<" "<<i<<endl;
tag[ax[i]]=1;
ans*=(ppow(ax[i],min(aa[i],bb[kk])))%mod;
ans%=mod;
}
}
for(int i=1;i<=cntb;i++)
{
int kk=finda(bx[i]);
if(kk>0&&!tag[bx[i]])
{
tag[bx[i]]=1;
ans*=(ppow(bx[i],min(aa[kk],bb[i])))%mod;
ans%=mod;
}
}
T2 距离之和
更令人忍俊不禁。
考场上想到二分优化的做法,但是二分一直神奇地出问题。
二分:
考虑每个转移,只会影响 \(x\) 轴或 \(y\) 轴上的距离。
- 如果是
S
或J
,那么 \(x\) 轴方向上的距离都不会变 - 如果是
I
或Z
,那么 \(y\) 轴方向上的距离都不会变
我们可以考虑分别以纵坐标和横坐标为关键字,对所有控制点进行排序。
然后就可以二分查找找到当前机器人坐标的相对位置,并记录其变化量就能得出答案。
还有两种说法,第一种用了权值线段树,60 分;
第二种 lhx 用了计数前缀和直接秒掉二分。
题解给的正解就是二分!!!!!
T3 country
T4 太空飞船
2024.2.19 【寒假集训】20240219测试
T1 素数
小黄题,无话可说。
搞前缀和就行了。
T2 晨练
小 \(dp\),是我的能力范围之外,用 \(dfs\) 骗了 20 分。
定义 \(dp[i][j]\) 表示在第 \(i\) 分钟,疲劳度为 \(j\) 时的最长跑步距离。
对于每一个 \(i\),有四种状态:
-
疲劳度不为 \(0\),且要继续跑
-
疲劳度不为 \(0\),且要休息
-
疲劳度为 \(0\),且要开始跑
-
疲劳度为 \(0\),且要休息
由此,我们分类讨论,可以得到:
#include<bits/stdc++.h>
#define int long long
using namespace std;
int n,a[10005];
int dp[10005][505];
int m;
signed main()
{
cin>>n>>m;
for(int i=1;i<=n;i++) cin>>a[i];
dp[1][1]=a[1];
for(int i=1;i<=m;i++) dp[0][i]=-1145141919;
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
dp[i][j]=dp[i-1][j-1]+a[i];
// dp[i+j][0]=max(dp[i][j],dp[i+j][0]);
}dp[i][0]=dp[i-1][0];
for(int j=i-1,k=1;j&&k<=m;j--,k++)
{ //j-i-p,k=j+p
dp[i][0]=max(dp[i][0],dp[j][k]);
}
}
cout<<dp[n][0];
}
T3 奇怪的桌子
神秘组合 dp,用纯组合骗了 40 分。
那就是当 \(n=m\) 时,答案是 \(\large{C_{n^2}^k}\)。
通过模拟,我们可以知道对于第 \(i\) 列和第 \(i % n\) 列的放法一定是相同的
考虑 dp,令 \(dp[i][j]\) 表示第 \(i\) 列放 \(j\) 个的方案数,
通过神秘的感性理解,可以得到状态转移方程:
其中 \([i \le m \% n]\) 表示当 \(i \le m \% n\) 成立时为 \(1\),否则为 \(0\) (一种神秘的 bool 表达式)。
但是,这样写会 T 掉几个点,必须先预处理 \(\large{C_{n}^{\frac{m}{n} +[i \le m \% n]}}\) 这一堆。
预处理后时间复杂度为 \(O(n^3)\) 左右(也有说法是 \(O(n^3 + n \times \max(n,k))\))。
dp[0][0]=1;
for(int i=1;i<=n;i++)//预处理
{
for(int j=0;j<=max(n,k);j++)
{
qp[i][j]=ppow(getc(n,j),(m/n+(i<=m%n)))%mod;
}
}
for(int i=1;i<=n;i++)
{
for(int j=0;j<=min(i*n,k);j++)
{
for(int l=0;l<=min(n,j);l++)
{
dp[i][j]=(dp[i][j]+(dp[i-1][j-l]*qp[i][l])%mod)%mod;
}
}
}
cout<<dp[n][k];
T4 学校
神秘最短路+\(tarjan\) 求割边。
2024.2.21【寒假集训】20240221测试
寄的最惨的一次。。
T1 排序
这是个数学题。
给定 \(n\) 和 \(4n\) 个元素,要求将其分为 \(n\) 组,使得对于每组四个数 \(a,b,c,d\),所有组中 \(∣ab−cd∣\) 的和最大,求最大和。
一开始我看了一眼,没多想就去看 T2 T3 T4 了,结果 T4 写了两个小时挂了,回来再看 T1 已经神志不清了。。
现在用不等式证明一下:
设有 \(8\) 个数,分别为 \(a_1,a_2,a_3,a_4\),并且 \(a_1>a_2>a_3>a_4\)。
我们可以知道,所有的 \(ab-cd\),有这几种可能:
我们要找 \(a_1a_2-a_3a_4\) 和 \(a_1a_3-a_2a_4\) 的关系,可以这么写:
其他证明同理。
最终我们可以得到这个不等式链:
所以,大的数应该相邻搭配,小的数应该一大一小搭配。
用样例去不完全归纳也可以得到这个结论。
答案序列:\(5,5,3,1 \ \ 4,3,2,1\)
#include<bits/stdc++.h>
#define int long long
using namespace std;
int n;
int a[400005],ans;
signed main()
{
cin>>n;
for(int i=1;i<=4*n;i++)
{
cin>>a[i];
}
sort(a+1,a+4*n+1);
for(int i=1;i<=n;i++)
{
ans+=a[2*n+2*i-1]*a[2*n+2*i]-a[i]*a[2*n-i+1];
}
cout<<ans;
return 0;
}
T2 牛吃草
神秘 dp,是我的认知范围之外:所有我解释不了的题目不是神秘 dp 就是科技数据结构。
首先二分答案 \(size\),对于每个 \(size\),跑一遍神秘 dp 计算答案:
令 \(dp[i]\) 表示考虑完 \([1,i]\) 后得到的最大覆盖长度。
则:
神秘 dp 跑完之后,如果 \(dp[n]\ge m\),\(m\) 表示\(\frac{s \times n}{100}\),就记录答案。
这样能搞到 75。
bool ck(int siz)
{
for(int i=1;i<=n;i++) dp[i]=0;
for(int i=1;i<=n;i++)
{
dp[i]=dp[i-1];
for(int j=i-a[i];j<=i-siz;j++)
dp[i]=max(dp[i],dp[j]+(i-j));
}
if(dp[n]>=m) return 1;
return 0;
}
考虑优化:
观察:
可以感性地得到:
下限单调不降,相当于滑动窗口 \([i-w_i,i-size]\),可以单调队列维护。
#include<bits/stdc++.h>
using namespace std;
int n,a[1000101];
int ans;
double m;
int dp[1000010];
int q[1001001];
bool ck(int siz)
{
for(int i=1;i<=n;i++) dp[i]=0,q[i]=0;
int tt=1,hh=1;q[1]=1;
for(int i=siz;i<=n;i++)
{
if(a[i]<siz)
{
dp[i]=dp[i-1];continue;
}
while(hh<=tt&&dp[i-siz]-i+siz>=dp[q[tt]]-q[tt]) tt--;
q[++tt]=i-siz;
while(hh<=tt&&i-a[i]>q[hh])hh++;
dp[i]=max(dp[i-1],dp[q[hh]]+i-q[hh]);
}
if(dp[n]>=m) return 1;
return 0;
}
int main()
{
cin>>n;
for(int i=1;i<=n;i++) cin>>a[i];
int s;cin>>s;
m=ceil(n*s/100.0);
int l=1,r=n;
while(l<=r)
{
int mid=(l+r)>>1;
if(ck(mid))
ans=mid,l=mid+1;
else r=mid-1;
}
cout<<ans;
}
2024.2.22【寒假集训】20240222测试
T1 打赌
大大大模拟。
空空空间思维。
判判判断。
这样我们可以找规律:
- 当 \(c \mod 4=1\) 时,列以 \(4\) 个一循环。
- 当 \(c \mod 4=2\) 时,列以 \(6\) 个一循环。
- 当 \(c \mod 4=3\) 时,列以 \(2\) 个一循环。
- 当 \(c \mod 4=0\) 时,列以 \(1\) 个一循环。
然后对于循环,预处理出每一列的和,最后一加就可以了。
搞了一个比较骚的 \(namespace\)。
#include<bits/stdc++.h>
#define int long long
using namespace std;
int n,c;
int line[7];
namespace Rotation{
void rotateleft(int &top,int &bottom,int &left,int &right)
{
int tmp=top;
top=left,left=bottom,bottom=right,right=tmp;
return ;
}
void rotatefront(int &top,int &bottom,int &front,int &back)
{
int tmp=top;
top=back,back=bottom,bottom=front,front=tmp;
return ;
}
void rotateright(int &top,int &bottom,int &left,int &right)
{
int tmp=top;
top=right,right=bottom,bottom=left,left=tmp;
return ;
}
}
void init()
{
int top=1,bottom=6,front=2,back=5,left=3,right=4;
for(int k=1;k<=6;k++)
{
int cnt=0;
for(int i=1;i<c;i++)
{
cnt+=top;
if(k%2==1)
Rotation::rotateright(top,bottom,left,right);
else
Rotation::rotateleft(top,bottom,left,right);
}cnt+=top;
line[k]=cnt;
Rotation::rotatefront(top,bottom,front,back);
}
}
int cntline=0;
signed main()
{
cin>>n>>c;
init();int tp=c%4,kk=0;
if(tp==1) kk=4;
else if(tp==2) kk=6;
else if(tp==3) kk=2;
else kk=1;
for(int i=1;i<=kk;i++) cntline+=line[i];
cntline*=(n/kk);
for(int i=1;i<=n%kk;i++) cntline+=line[i];
cout<<cntline;
}
T2 舞会
对于这种不需要动脑的 ** 数据结构题,就不要考虑朴素算法。
我们考虑使用权值线段树去模拟平衡树操作。
但是不会。
那我们可以使用一个万能小 \(map\) 加上万能\(lower(upper) \_ bound\) 帮助我们找一个数的前驱和后继。
这样权值线段树就起到了辅助我们判断是否有合法的匹配,\(map\) 来找前驱后继。
然后分别在这两个数据结构里删点即可。
正数和负数要开两个。
要注意判断是否有合法时 \(now\) 要加一或减一。
码子有点长,有点丑,放到里头了:
#include<bits/stdc++.h>
using namespace std;
int n,a[101000],b[101000];
struct segmentTree{
public:
class node{
public:
int sum,l,r;
}tr[30000<<2];
#define lid now<<1
#define rid now<<1|1
void update(const int now,const int l,const int r,const int x,const int y,const int k)
{
if(x<=l&&r<=y)
{
tr[now].sum+=k;return ;
}
const int mid=(l+r)>>1;
if(x<=mid) update(lid,l,mid,x,y,k);
if(y>mid) update(rid,mid+1,r,x,y,k);
tr[now].sum=tr[lid].sum+tr[rid].sum;
}
int query(const int now,const int l,const int r,const int x,const int y)
{
if(x<=l&&r<=y) return tr[now].sum;
const int mid=(l+r)>>1;
int res=0;
if(x<=mid) res+=query(lid,l,mid,x,y);
if(y>mid) res+=query(rid,mid+1,r,x,y);
return res;
}
}st[2];
long long ans;
map<int,int>gir1,gir0;
int main()
{
cin>>n;
for(int i=1;i<=n;i++)
{
int t;cin>>t;
st[t>=0].update(1,1,2500,abs(t),abs(t),1);
if(t>0) gir1[t]++;
else gir0[-t]++;
}
for(int i=1;i<=n;i++)
{
int now;cin>>now;
if(now>0)
{
if(st[0].query(1,1,2500,now+1,2500)>=1)
{
int kk=(gir0.upper_bound(now))->first;
++ans;
st[0].update(1,1,2500,kk,kk,-1);
gir0[kk]--;
if(gir0[kk]==0) gir0.erase(kk);
}
}
else
{
if(st[1].query(1,1,2500,0,-now-1)>=1)
{
int kk=(--gir1.lower_bound(-now))->first;
++ans;
st[1].update(1,1,2500,kk,kk,-1);
gir1[kk]--;
if(gir1[kk]==0) gir1.erase(kk);
}
}
}
cout<<ans;
}
T3 最小生成树
这个题看似是一个图论题,实际上是数学题。
- 对于要留下的边的个数,我们知道是要互质的。
答案就是每个点欧拉函数乘积。
可以使用 wme 大佬的线性筛,也可以使用简单质朴的神秘筛法:
inline int euler()
{
for(int i=1;i<=n;++i) phi[i]=i;
for( int i=2;i<=n;++i)
{
if(phi[i]==i)
{
for( int j=i;j<=n;j+=i)
phi[j]=phi[j]/i*(i-1);
}
}
}
T4 买汽水
这道题可以使用神秘 dp 骗到 100 分。
但是正解是搜素,对半搜索加上神秘剪枝。
参考:(不是我写的)
#include<bits/stdc++.h>
#define ll long long
using namespace std;
int n;
ll ans=0,m,a[50];
void dfs(int x,ll k){
if(x==n+1){
if(k<=m) ans=max(ans,k);
return ;
}
k+=a[x];
if(k==m||ans==m){//再加点剪枝?
ans=m;
return ;
}
if(k<=m) dfs(x+1,k);
k-=a[x];
dfs(x+1,k);
}
int main(){
scanf("%d%lld",&n,&m);
ll sum=0;
for(int i=1;i<=n;i++){
scanf("%lld",&a[i]);
sum+=a[i];
}
if(sum<=m){
printf("%lld",sum);
return 0;
}
dfs(1,0);
printf("%lld",ans);
return 0;
}
20240405测试
我爱期望。。。
T1 [JLOI2014] 聪明的燕姿
这是数学。用线性筛去筛约数和然后循环判断可以拿 \(28\) 分。
T2 luogu4550收集邮票
纯期望。
考虑倒推,取 \(i\) 次剩下的期望 \(f[i]=f[i+1]\times \frac{n}{n-i}\)。
取 \(i\) 次的期望得分 \(g[i]=\frac{i}{n-i}+g[i+1]+f[i+1]\times f[i]+\frac{n}{n-i}\)
for(int i=n-1;~i;i--)
{
f[i]=f[i+1]+1.0*n/(1.0*(n-i));
g[i]=(1.0*i)/(1.0*n-i)*f[i]+g[i+1]+f[i+1]+n/(1.0*(n-i));
}
答案就是 \(g[0]\)。
20240503测试
T1 [CF1279C]Stack of Present
就是小贪心,记录探过的最深的地方,每次更新。
T2 [luogu5522]棠梨煎雪
T3 [luogu1174]打砖块
神秘三维 dp,维护 \(dp[i][j][0/1]\) 表示到第 \(i\) 列,用过 \(j\) 颗子弹的最大得分,\(0/1\) 表示这一列有没有向前面借子弹。
T4 「NOIP2015」斗地主
超级神秘大模拟,使用 \(dfs\) 加回溯。
这是搜索的顺序,至于为啥,咱也不知道,咱也不敢问。
心肺骤停。。。就说为啥调不出来。
这直接无缝衔接,把小王大王读成 \(A\) 了。。。
END
\(\frak{set \ up \ on \ 24.2.6}\)
\(\frak{upd \ on \ 24.2.15}\)