[35] (CSP 集训) CSP-S 模拟 5
T1 光
Hikari
好神秘这个题,我觉得我解法够神秘了结果是正解
考虑到这四个数虽然不能二分答案,但是它们的和是能二分答案的
因此对和做二分答案
然后问题变成了 check 怎么写
设和最小的答案为 \((i_1,i_2,i_3,i_4)\)
注意到 \(n\) 只有 \(1500\),考虑直接 \(n^2\) 枚举前两个数
那么后面俩咋办
注意到可以令 \(i_4=sum-i_1-i_2-i_3\)
把题目里的四个限制移项,移项之后等式左边还剩四组
\(i_3+\lfloor\frac{i_4}{2}\rfloor\ge\) 一坨常数
\(i_4+\lfloor\frac{i_3}{2}\rfloor\ge\) 一坨常数
\(\lfloor\frac{i_3}{2}\rfloor+\lfloor\frac{i_4}{4}\rfloor\ge\) 一坨常数
\(\lfloor\frac{i_4}{2}\rfloor+\lfloor\frac{i_3}{4}\rfloor\ge\) 一坨常数
对于前两组,发现当 \(i_3\) 增大的时候,第一组的值增大,第二组的值减小
说明还是有单调性,继续对 \(i_3\) 二分答案。
后面两组没办法了,因为挂 floor,只能说看起来和前面两组差不多,其实并不是单调的,可以自己试试
所以充分发扬人类智慧,先判前两组限制,前两组过了说明离答案非常近了,所以直接在答案区间左右 \(d\) 的范围内暴力 check 是否合法,合法直接返回,\(d\) 取的 \(8\),因为分母是 \(4\)
最坏复杂度 \(n^2\log(4n)\log(n+8)\) 不知道为啥这么快
非常卡常,记得常数优化
#include<bits/stdc++.h>
using namespace std;
int a,b,c,d;
//判断 (i1,i2,i3,i4) 是否合法
inline int check(int i1,int i2,int i3,int i4){
if(i3+(i4/2)<c-(i1/2)-(i2/4)){
return 1;//i3 小了
}
if(i4+(i3/2)<d-(i1/4)-(i2/2)){
return -1;//i3 大了
}
if((i3/2)+(i4/4)<a-i1-(i2/2)){
return 0;//接神秘做法
}
if((i4/2)+(i3/4)<b-i2-(i1/2)){
return 0;
}
return 2;//正好是对的
}
//二分答案 i3
inline bool checksum2(int sum,int i1,int i2){
int remain=sum-i1-i2;
int l=0,r=remain;
while(l<=r){
int i3=(l+r)/2;
int i4=remain-i3;
int res=check(i1,i2,i3,i4);
if(res==2) return true;
if(res==1){
l=i3+1;
}
if(res==-1){
r=i3-1;
}
if(res==0){
for(int i=max(0,i3-8);i<=min(remain,i3+8);++i){
//暴力
if(check(i1,i2,i,remain-i)==2){
return true;
}
}
return false;
}
}
return false;
}
//二分答案 sum 的 check() 函数
inline bool checksum(int sum){
for(int i1=0;i1<=sum;++i1){
for(int i2=0;i2<=sum-i1;++i2){
if(i1==114 and i2==22){
}
if(checksum2(sum,i1,i2)){
return true;
}
}
}
return false;
}
signed main(){
// freopen("test.in","r",stdin);
// freopen("test.out","w",stdout);
freopen("light.in","r",stdin);
freopen("light.out","w",stdout);
scanf("%d %d %d %d",&a,&b,&c,&d);
int l=0,r=a+b+c+d,ans=-1;
//二分答案 sum
while(l<=r){
int mid=(l+r)/2;
if(checksum(mid)){
r=mid-1;
ans=mid;
}
else{
l=mid+1;
}
}
cout<<ans<<'\n';
}
/*
50 24 25 12
*/
B.爬
爬了
没用大家的思路,但是还是殊遇同归了
首先考虑计算在每个节点处形成的的答案值,发现这样的答案只会涉及到它本身与它的下一级节点,因此考虑全部提出来
提出来之后,这些所有数形成了一个序列,而你需要做的是算出所有子序列的异或和的和(不包括空序列与长度为 \(1\) 的序列,然而这个简单,直接减掉就行了),暂且先不说怎么算
考虑算出来之后干什么,注意到不在这个范围内的节点进行移动是不会对这个范围内的答案造成影响的,因此直接将这个答案乘以 \(2^{s}\),然后加到总答案里
\(s\) 是满足如下要求的结点个数
- 可以自由移动(即不能是根节点)
- 不是当前节点及其下一级子节点
这里需要额外注意的是统计根节点的答案时候的情况,注意不要多减
然后说怎么算这个所有子序列的异或和的和
对序列里的所有数拆二进制位,考虑第 \(i\) 位,显然,只有挑出奇数个该位上为 \(1\) 的数拼起来,才能对答案有贡献,剩下的该位为 \(0\) 的数随便选即可,需要注意的是当挑出 \(1\) 个该位上为 \(1\) 的数,\(0\) 个该位上为 \(0\) 的数时是不符合题意的,所以减掉,有贡献的总方案数为 \((\sum\limits_{j\in\{1,3,5,7\cdots\}}C_{cnt1}^{j})\times2^{cnt0}-C_{cnt1}^{1}\),其次,该位的每一个方案对整体答案的总贡献为 \(2^{i-1}\),统计即可
值得注意的是根节点处的统计,根节点无法移出,意思是必须选 \(a_{root}\),那么先对 \(a_{root}\) 二进制拆分,在 \(a_{root}\) 为 \(1\) 的二进制位上选偶数个为 \(1\) 的即可,需要注意 \(0\) 也算偶数(此外,\(a_{root}\) 为 \(0\) 的位置上不需要减掉非法状态)
复杂度大概是线性乘以啥,里面复杂度不会算
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int p=1e9+7;
int n;
vector<int>e[100001];
int a[100001];
int power(int a,int t){
int base=a,ans=1;
while(t){
if(t&1){
ans=ans*base%p;
}
base=base*base%p;
t>>=1;
}
return ans;
}
//以下卢卡斯相关
long long fact[100001];
void dofact(){
fact[0]=1;
for(int i=1;i<=100000;++i){
fact[i]=fact[i-1]*i%p;
}
}
long long C(long long n,long long m){
if(!m){
return 1;
}
if(n<m){
return 0;
}
if(m>n-m){
m=n-m;
}
long long ans=fact[n]*power(fact[m],p-2)%p*power(fact[n-m],p-2)%p;
return ans;
}
long long lucas(long long n,long long m){
if(!m) return 1;
return lucas(n/p,m/p)*C(n%p,m%p)%p;
}
//存储 0,1 的个数
int cnt0[33],cnt1[33];
int solve2(vector<int>&val,bool limit){//limit==true 是根节点
memset(cnt0,0,sizeof cnt0);
memset(cnt1,0,sizeof cnt1);
//拆位
for(int p=0;p<=(int)val.size()-(limit?2:1);++p){
int i=val[p];
for(int j=1;j<=32;++j){
if(i&1){
cnt1[j]++;
}
else{
cnt0[j]++;
}
i>>=1;
}
}
int ans=0;
for(int i=1;i<=32;++i){
int res=0;
for(int j=((limit and ((val.back()>>(i-1))&1)==1)?0:1);j<=cnt1[i];j+=2){
res+=lucas(cnt1[i],j);
res%=p;
}
res=res*power(2,cnt0[i])%p;//0 的位置随便排
if(!limit or ((val.back()>>(i-1))&1)==1)res-=lucas(cnt1[i],limit?0:1);//减不合法
ans+=res*power(2,i-1);//该位贡献量
ans%=p;
}
return ans;
}
int dfs(int now,int last){
int res=0,fas=n-1-(now!=1);//fas 即为上述 s
vector<int>val;
for(int i:e[now]){
if(i!=last){
fas--;
res+=dfs(i,now);
res%=p;
val.push_back(a[i]);
}
}
val.push_back(a[now]);
int rrres=solve2(val,now==1);
res+=(rrres)%p*power(2,fas)%p;
return res%p;
}
signed main(){
freopen("climb.in","r",stdin);
freopen("climb.out","w",stdout);
dofact();
scanf("%lld",&n);
for(int i=1;i<=n;++i){
scanf("%lld",&a[i]);
}
for(int i=2;i<=n;++i){
int x;scanf("%lld",&x);
e[x].push_back(i);
e[i].push_back(x);
}
cout<<dfs(1,0)%p;
}
C. 字符串
50 分 DP 是好写的
\(f_{i,j,k,0/1}\) 是已经使用 \(i\) 个 A
,\(j\) 个 B
,最后连续段长度为 \(k\),最后为 A
(\(0\)) 或 B
(\(1\))
转移的时候直接填就行了,太简单了不赘述
#include<bits/stdc++.h>
using namespace std;
int t;
int n,m,a,b,c;
int f[101][101][101][2];//0: last is A 1: last is B
int main(){
freopen("string.in","r",stdin);
freopen("string.out","w",stdout);
scanf("%d",&t);
while(t--){
scanf("%d %d %d %d %d",&n,&m,&a,&b,&c);
memset(f,0,sizeof f);
f[0][0][0][0]=f[0][0][0][1]=1;
for(int i=0;i<=n;++i){
for(int j=0;j<=m;++j){
for(int k=1;k<=i;++k){
if(i!=0 and j!=0) f[i][j][1][1]=max(f[i][j][1][1],f[i][j-1][k][0]+1);
}
for(int k=1;k<=j;++k){
if(i!=0 and j!=0) if(k>=c) f[i][j][1][0]=max(f[i][j][1][0],f[i-1][j][k][1]+1);
}
for(int k=1;k<=i;++k){
if(i!=0) f[i][j][k][0]=max(f[i][j][k][0],f[i-1][j][k-1][0]+(k!=1 and (k-1)%a==0));
}
for(int k=1;k<=j;++k){
if(j!=0) f[i][j][k][1]=max(f[i][j][k][1],f[i][j-1][k-1][1]+(k!=1 and (k-1)%b==0));
}
}
}
int ans=0;
for(int i=0;i<=1;++i){
for(int j=0;j<=(i==0?n:m);++j){
ans=max(ans,f[n][m][j][i]);
}
}
cout<<ans<<'\n';
}
}
题解解析写挺好,那我就不用写了
我们可以枚举 A
和 B
切换了多少次,假定我们切换的方式就是 BBBABBBABBBA
,即每一段 A
的长度都为 \(1\),我们又知道 A
B
的切换次数,就可以知道现在已经用掉了几个 A
,然后我们先考虑把 A
全部用完。
- 在
BBBABBBA
的形式前面 只需要花费一个A
就可以拿到一个价值 - 然后将多余的
A
填充进字符串中,因为每一段A
的长度都是 \(1\),所以此时本质上是每多 \(a\) 个A
,答案 \(+1\)。
然后再考虑将剩余的 B
用完。
- 如果倒数第二个位置是
A
的话,那么最后一个只需要花费一个B
就能拿到一个价值 - 例如 \(b=4,c=3\) ,本来我们填充的是
BBBABBBABBBA
,根据规则 2,当一段B
的长度达到 \(5\) 的时候又可以使得价值 \(+1\),所以我们尽量将每一段B
都填充到长度为 \(5\) 。如果全部填充好了且还有多余的 \(B\) ,那么每多 \(b\) 个 \(B\),答案 \(+1\)。
一些细节在代码里贴了注释,如果你能看懂我在说啥的话
#include<bits/stdc++.h>
using namespace std;
int main(){
freopen("string.in","r",stdin);
freopen("string.out","w",stdout);
int t;
cin>>t;
while(t--){
int n,m,a,b,c;
cin>>n>>m>>a>>b>>c;
int maxn=0;
for(int i=0;i<=min(n,m/c);i++){
//i stands for the switch times
//we need to put c 'b' together to switch to a,so there is a m/c
//and this min means the max time we can switch
int sum=0;
sum+=i*2-1;
//this -1 means the switch at begin, bacause the begin switchness only 1 times but not 2
//i*2 is the switch score totally, *2 because there both a to b and b to a
//we do not care the last switch if it is not the same as the first switch
if(n-i>=1){
sum+=1+(n-i-1)/a;
//except i a that used to switch, put others a together and insert after one a
//+1 because we can insert a 'a' before B...BAB...BAB...BA
//-1 because we used one to +1
}
if(c>b){
sum+=(c-1)/b*i;
//cals if a area lengths c make any score
}
//bacause i play genshin impact
if(m-c*i>=1){
//try to make every B area lengths to b+1
int x=m-i*c-1;//remain B
int mod=(((c-1)/b+1)*b)-(c-1); //needs to be next b+1
if(x/mod>=i){
sum+=i;
x-=mod*i;
}
else{
sum+=x/mod;
x-=x/mod*mod;
}
sum+=1+x/b;
//x/b because if we have too much b, we can make it at back to make scores
}
maxn=max(maxn,sum);
}
cout<<maxn+1<<'\n';
}
}
D. 奇怪的函数
感谢 Qyun 提供的思路
奇怪的分块妹妹助我过奇怪的函数题
关于为什么这玩意一定是一个三段函数
初始时,有 \(y=x\),函数为一条斜率为 \(1\) 的直线
加和操作,使函数整体平移,形态不变
\(\min,\max\) 操作,在函数上,下截取,使其上下出现平台,但是因为 \(\min,\max\) 会互相覆盖,因此只会出现至多两个平台
因此是三段函数
考虑怎么求拐点
求拐点的实质是维护最大最小值,由于整体加和操作会导致图像整体平移,不妨设 \(dx\) 为整体偏移量,那么我们就只需要考虑最大最小值是什么即可
由于我们已经定义了偏移量,所以最大最小值只需要扫一遍取 \(\min,\max\) 即可
有一些问题,\(\min\) 规定了函数的最大值,\(\max\) 规定了函数的最小值,这是需要注意的
如果新加入的 \(\min\) 比 \(\max\) 还小怎么办,这个时候应该让之前的 \(\max\) 发挥作用,由于之前的 \(\max\)(即函数下界) 比新加入的 \(\min\)(新的函数上界)小,因此函数图像应为一条直线,此时有 \(\min=\max\)
这是预处理的一些细节
其余直接对操作分块即可,在每个块内分别求值,求出来带进下一个块内
注意由于真实的 \(\max\) 也是 \(\max+dx\),赋值的时候注意加
预处理一定要倒着做,后赋的值比先赋的值优先级要高
#include<bits/stdc++.h>
using namespace std;
int n,q;
class huge{
public:huge(){}
operator int(){return 0x3f3f3f3f;}
}blublu;
struct operat{
int id;
int vali;
}op[100001];
int len;
struct area{
int minn,maxn,dx;
}a[100001];
huge act(int id){
a[id]={-blublu,blublu,0};
for(int i=min(n-1,(id+1)*len-1);i>=id*len;--i){
if(op[i].id==1){
a[id].dx+=op[i].vali;
a[id].maxn-=op[i].vali;
a[id].minn-=op[i].vali;
}
if(op[i].id==2){
a[id].maxn=min(a[id].maxn,max(a[id].minn,op[i].vali));
}
if(op[i].id==3){
a[id].minn=max(a[id].minn,min(a[id].maxn,op[i].vali));
}
}
return huge();
}
huge prework(){
len=sqrt(n);
for(int i=0;i<=(n-1)/len;++i){
act(i);
}
return huge();
}
int solve(int id,int x){
if(x<=a[id].minn) return a[id].minn+a[id].dx;
if(x>=a[id].maxn) return a[id].maxn+a[id].dx;
return x+a[id].dx;
}
int f(int x){
for(int i=0;i<=(n-1)/len;++i){
x=solve(i,x);
}
return x;
}
int main(){
freopen("function.in","r",stdin);
freopen("function.out","w",stdout);
cin>>n;
for(int i=0;i<=n-1;++i){
cin>>op[i].id>>op[i].vali;
}
prework();
cin>>q;
while(q--){
int _op,pos,val,x;
cin>>_op;
if(_op==1){
cin>>pos>>val;pos--;
op[pos].id=1;
op[pos].vali=val;
act(pos/len);
}
if(_op==2){
cin>>pos>>val;pos--;
op[pos].id=2;
op[pos].vali=val;
act(pos/len);
}
if(_op==3){
cin>>pos>>val;pos--;
op[pos].id=3;
op[pos].vali=val;
act(pos/len);
}
if(_op==4){
cin>>x;
cout<<f(x)<<'\n';
}
}
}
/*
10
1 48
1 50
1 180
2 957
1 103
1 100
1 123
3 500
1 66
1 70
3
4 20
4 50
4 700
*/