2024暑假集训测试2
前言
- 比赛链接。
T1、T4 比较简单,打完基本就罚坐了,想了三个小时的 T2、T3 也没想出来。
T1 酸碱度中和
二分答案加贪心即可,先排序,每瓶可装 \(a_i\sim a_i+2*m\)。
点击查看代码
#include<bits/stdc++.h>
#define int long long
#define endl '\n'
#define sort stable_sort
using namespace std;
const int N=1e5+10;
template<typename Tp> inline void read(Tp&x)
{
x=0;register bool z=true;
register char c=getchar();
for(;c<'0'||c>'9';c=getchar()) if(c=='-') z=0;
for(;'0'<=c&&c<='9';c=getchar()) x=(x<<1)+(x<<3)+(c^48);
x=(z?x:~x+1);
}
void wt(int x){if(x>9)wt(x/10);putchar((x%10)+'0');}
void write(int x){if(x<0)putchar('-'),x=~x+1;wt(x);}
int n,m,l,r,mid,a[N],maxx,ans;
bool check(int x)
{
int sum=0,now=a[1];
for(int i=1;i<=n+1;i++)
if(a[i]-now>2*x)
{
now=a[i];
sum++;
if(sum>m) return 0;
}
return (sum<=m);
}
signed main()
{
read(n),read(m);
for(int i=1;i<=n;i++)
read(a[i]),
maxx=max(maxx,a[i]);
sort(a+1,a+1+n);
a[n+1]=0x7f7f7f7f;
l=0,r=maxx;
while(l<=r)
{
mid=(l+r)>>1;
if(check(mid)) r=mid-1,ans=mid;
else l=mid+1;
}
write(ans);
}
T2 聪明的小明
-
部分分 \(5pts\):\(k=m\) 时答案为 \(k!\)。
-
部分分 \(25pts\):\(n\le 20\& k=2\) 时爆搜。
-
部分分 \(45pts\):\(k=2\) 时状压,如何状压在正解里说。
-
正解:
鉴于 \(m\le 10\) 且内存给了 \(1G\),考虑状压。
状压的思路比较巧妙,对于长度为 \(m\) 的 \(01\) 串,\(1\) 表示某数在这 \(m\) 位中最后一次出现在此处,\(0\) 则表示该数在后面还出现过,如
011
可以表示为112、221、121、212
等。之所以要这么状压是因为需要处理第 \(m+1\) 位和第 \(1\) 位之间的影响。
对于每一个状态,若 \(1\) 的个数恰好 \(=k\) 且最后一位为 \(1\) 时为合法状态。
继续考虑每一种状态对应多少种情况,从后向前遍历,记录已经遍历到的 \(1\) 的个数,若遇到一个 \(1\),答案 \(\times (k-num)\),然后 \(sum+1\),若为 \(0\) 则 \(\times num\),正确性显然。
继续考虑如何转移,有 \(f_{i,s}=\sum f_{i-1,s'}\),其中 \(s\) 可以从 \(s'\) 转移过来。
继续考虑一种状态 \(s\) 可以转化成什么状态,若 \(s\) 的第一位为 \(1\),说明该数仅出现过一次,则 \(m+1\) 位也必须为该数,如
101
尽可以转化为011
。对于其余情况的状态,每一个 \(1\) 都可以移到 \(m+1\) 位,此位变为 \(0\),如
011
可以转化位011
和101
。复杂度为 \(O(n2^m)\),若提前处理出所有合法状态的话可将复杂度优化到 \(O(nC_m^k)\),\(m=10,k=5\) 时取到最大为 \(O(252n)\)。
点击查看代码
#include<bits/stdc++.h> #define int long long #define endl '\n' #define sort stable_sort using namespace std; const int N=1e5+2,P=998244353; template<typename Tp> inline void read(Tp&x) { x=0;register bool z=true; register char c=getchar(); for(;c<'0'||c>'9';c=getchar()) if(c=='-') z=0; for(;'0'<=c&&c<='9';c=getchar()) x=(x<<1)+(x<<3)+(c^48); x=(z?x:~x+1); } void wt(int x){if(x>9)wt(x/10);putchar((x%10)+'0');} void write(int x){if(x<0)putchar('-'),x=~x+1;wt(x);} int n,k,m,f[N][1025],c[1025],ans; int check(int sta) { if(c[sta]!=-1) return c[sta]; if((sta&1)==0) return c[sta]=0; int sum=0,ans=1,now=sta; for(int i=1;i<=m;i++) { if(sta&1) (ans*=(k-sum))%=P, sum++; else (ans*=sum)%=P; sta>>=1; } return c[now]=(sum==k)*ans; } void solve(int i,int sta) { if(check(sta)==0) return ; if((sta>>(m-1))&1) { int now=sta; sta^=(1<<(m-1)); sta<<=1; sta|=1; (f[i+1][sta]+=f[i][now])%=P; return ; } for(int j=1;j<=m;j++) if((sta>>(j-1))&1) { int now=sta; sta^=(1<<(j-1)); sta<<=1; sta|=1; (f[i+1][sta]+=f[i][now])%=P; sta=now; } } signed main() { memset(c,-1,sizeof(c)); read(n),read(k),read(m); for(int s=1;s<=(1<<m)-1;s+=2) f[m][s]=check(s); for(int i=m;i<=n-1;i++) for(int s=1;s<=(1<<m)-1;s+=2) solve(i,s); for(int s=1;s<=(1<<m)-1;s+=2) (ans+=f[n][s])%=P; write(ans); }
T3 线段树
定义 \(f_{l,r}\) 为区间 \(l\sim r\) 的贡献,有 \(f_{l,r}\) 下界为 \(1\)。
对于线段树上一个区间 \([L,R]\),若存在一个询问区间 \([l,r]\) 与 \([L,R]\) 存在交集且 \([L,R]\) 不被 \([l,r]\) 包含,说明 \([l,r]\) 被分成了至少两个区间,由此 \(f_{L,R}\) 的贡献 \(+1\)。
由此我们处理出每个断点 \(i\) 被多少个询问区间包含,记为 \(w_i\),以及每个线段树上的区间 \([l,r]\) 被多少个询问区间包含,记为 \(sum_{l,r}\)。
其中 \(w_i\) 可以在输入的时候直接差分处理,\(sum_{l,r}\) 可以类似于二维前缀和和区间 \(DP\) 维护,有:
\(num_{l,r}\) 表示询问中恰好区间 \([l,r]\) 的个数。
最后考虑 \(DP\) 转移,\(f_{l,r}\) 为线段数上区间 \([l,r]\) 最少能提供多少除 \(m\) 个基本询问外额外的贡献,有:
初始值 \(f_{i,i}\) 为 \(0\),其余均赋成极大值,这与之前的 \(f_{l,r}\) 下界为 \(1\) 是矛盾的,但是鉴于对 \(f_{l,r}\) 的定义为额外贡献,其初始值应该为 \(0\),最后答案为 \(+m\) 即可。
最后答案为 \(f_{1,n}+m\)。
点击查看代码
#include<bits/stdc++.h>
#define int long long
#define endl '\n'
#define sort stable_sort
using namespace std;
const int N=510;
template<typename Tp> inline void read(Tp&x)
{
x=0;register bool z=true;
register char c=getchar();
for(;c<'0'||c>'9';c=getchar()) if(c=='-') z=0;
for(;'0'<=c&&c<='9';c=getchar()) x=(x<<1)+(x<<3)+(c^48);
x=(z?x:~x+1);
}
void wt(int x){if(x>9)wt(x/10);putchar((x%10)+'0');}
void write(int x){if(x<0)putchar('-'),x=~x+1;wt(x);}
int n,m,w[N],sum[N][N],f[N][N];
signed main()
{
read(n),read(m);
for(int i=1,l,r;i<=m;i++)
read(l),read(r),
w[l]++,w[r]--,
sum[l][r]++;
for(int i=1;i<=n;i++) w[i]+=w[i-1];
for(int i=n;i>=1;i--)
for(int l=1;l+i-1<=n;l++)
{
int r=l+i-1;
sum[l][r]=sum[l][r]+sum[l-1][r]+sum[l][r+1]-sum[l-1][r+1];
}
memset(f,0x3f,sizeof(f));
for(int i=1;i<=n;i++) f[i][i]=0;
for(int i=2;i<=n;i++)
for(int l=1;l+i-1<=n;l++)
{
int r=l+i-1;
for(int k=l;k<r;k++)
f[l][r]=min(f[l][r],f[l][k]+f[k+1][r]+w[k]-sum[l][r]);
}
write(f[1][n]+m);
}
T4 公路
贪心加模拟即可。
当前对于到达了车站 \(i\),继续向前找到一个 \(j\),\(j\) 分为两种情况:
- 在油箱允许的范围内找到的第一个油价小于 \(i\) 的,此时只需讲油加到足够支撑到车站 \(j\) 的即可。
- 在油箱允许的范围内没有找到任何一个油价小于 \(i\) 的,则在该范围内找到油价最小的一个车站 \(j\),此时直接将油箱加满,防止在更贵的地方买更多的油。
处理过后直接另 \(i=j\) 即可,同时维护一个变量保存油箱内还剩下多少油。
鉴于双指针做法,随机数据下复杂度近似于 \(O(n)\),油价单调递减数据下为严格 \(O(n)\),油价单调递增且 \(c\ge\sum\limits_{i=0}^{n-1}v_i\) 数据下复杂度退化为 \(O(n^2)\),在数据比较水的情况下可以通过此题。
利用单调队列优化可将复杂度优化为严格线性,懒得打了但是。
点击查看暴力代码
#include<bits/stdc++.h>
#define int long long
#define endl '\n'
#define sort stable_sort
using namespace std;
const int N=1e5+10;
template<typename Tp> inline void read(Tp&x)
{
x=0;register bool z=true;
register char c=getchar();
for(;c<'0'||c>'9';c=getchar()) if(c=='-') z=0;
for(;'0'<=c&&c<='9';c=getchar()) x=(x<<1)+(x<<3)+(c^48);
x=(z?x:~x+1);
}
void wt(int x){if(x>9)wt(x/10);putchar((x%10)+'0');}
void write(int x){if(x<0)putchar('-'),x=~x+1;wt(x);}
int n,m,d[N],a[N],f[N],ans,now;
signed main()
{
read(n),read(m);
for(int i=1,v;i<=n;i++)
read(v),
d[i]=d[i-1]+v;
for(int i=0;i<=n-1;i++) read(a[i]);
int j;
for(int i=0;i<=n-1;i=j)
{
j=i+1;
int k,minn=0x7f7f7f7f;
while(j<=n)
{
if(d[j]-d[i]>m) {j--; break;}
if(a[j]<a[i]) break;
if(a[j]<minn) minn=a[j],k=j;
j++;
}
if(a[j]<a[i])
ans+=(d[j]-d[i]-now)*a[i],
now=0;
else
{
j=k;
ans+=(m-now)*a[i];
now=m-d[j]+d[i];
}
}
write(ans);
}
粘一个官方题解的单调队列优化代码
#include<bits/stdc++.h>
#define ll long long
#define maxn 1000005
#define mod 1000000007
using namespace std;
ll n,C;
ll x[maxn],p[maxn];
pair<ll,ll> Q[maxn+maxn];
int head,tail;
int main()
{
cin>>n>>C;
for (int i=1;i<=n;i++) cin>>x[i];
for (int i=1;i<=n;i++) cin>>p[i];
head=1; tail=1;
Q[head]={1ll<<30,C}; //为了保证队列里总大小是C
ll sum=0;
for (int i=1;i<=n;i++)
{
ll tt=0;
//删掉末尾不如p[i]的决策
while (head<=tail && Q[tail].first>=p[i]) {tt+=Q[tail].second; tail--;}
Q[++tail]={p[i],tt};
//在队列的开头拿出x[i]的油来加
ll ret=x[i];
while (ret)
{
ll num=min(ret,Q[head].second);
sum+=Q[head].first*num; Q[head].second-=num; ret-=num;
if (Q[head].second==0) head++;
}
Q[++tail]={1ll<<30,x[i]}; //为了保证队列里总大小是C
}
cout<<sum<<endl;
}
总结
要学会面向数据范围,确定正确的思路,如 T2 想出状压。
好好复习一下区间 DP。