最近沙河好多人生病了,我也不能幸免,今天终于舒服了。补一补上周的老坑吧。
接下来尽量每天刷题。另外要加油学习汇编语言,优化我们的密码学竞赛程序。
官方题解参见:http://clatisus.com/BCPC2018_qualification_round
https://buaacoding.cn/problem/1914/index
非常容易设计出粗暴的dp。考虑 M [ i ] [ j ] M[i][j] M[i][j]表示当前考虑到第i个数字时,Alice的时间和ugo时间差为j时,单独练习的最小时间段数。更新无非就是三种方式,用 M [ i − 1 ] [ j ] M[i-1][j] M[i−1][j]更新 M [ i ] [ j + a [ i ] ] M[i][j+a[i]] M[i][j+a[i]], M [ i ] [ j − a [ i ] ] M[i][j-a[i]] M[i][j−a[i]],用 M [ i − 1 ] [ j ] + 1 M[i-1][j]+1 M[i−1][j]+1更新 M [ i ] [ j ] M[i][j] M[i][j]。问题落在了j的数量级太大上,考虑到j虽然大,但是取值不多(这是因为b-a存在限制,可知每个数在区间 [ k ∗ a − 50 ∗ 50 , k ∗ a + 50 ∗ 50 ] [k*a-50*50,k*a+50*50] [k∗a−50∗50,k∗a+50∗50]中,k取-50到50,所以对于i=50时,j总的数量的和不会超过5001*101=505101,而且这个估计很宽大了,实际上远小于这个值)。
于是,考虑用map/unordered_map来代替dp数组记录值。这里unordered_map表现了更好的效率,另外采用滚动的方式能有效减小内存。
注:官方题解采用了增加一维的方式而避免采用了库容器,有可能会更加高效。
#include
#include
using namespace std;
using LL=long long;
unordered_map<LL,int> M[2];
int t[55],n,a,b,T,o;
int main()
{
scanf("%d",&T);
M[0][0]=1;
while(T--)
{
scanf("%d%d%d",&n,&a,&b);
for(int i=1;i<=n;i++)
scanf("%d",&t[i]);
o=0;
M[o].clear();
M[o][0]=1;
for(int i=1;i<=n;i++)
{
o^=1;
M[o].clear();
for(auto &k:M[o^1])
{
int &t1=M[o][k.first+t[i]];
t1=min(k.second,t1==0?100:t1);
int &t2=M[o][k.first-t[i]];
t2=min(k.second,t2==0?100:t2);
int &t3=M[o][k.first];
t3=min(k.second+1,t3==0?100:t3);
}
}
printf("%d\n",M[o][0]-1);
}
return 0;
}
https://buaacoding.cn/problem/1917/index
水题,考虑枚举级数x,检查的时候,选取费用最小的x-n-1个EXP,剩余的吃糖,看是否超出费用。正解说需要二分答案,但是数据范围很小,所以这里直接枚举。
#include
#include
using namespace std;
int n,m,a,b,c,s[105],T;
bool check(int x)
{
for(int i=1;i<x;i++)
s[i]=(i*a+b)%c;
sort(s+1,s+x);
int res=0;
for(int i=1;i<x-n;i++)
res+=s[i];
return res<=m;
}
int main()
{
scanf("%d",&T);
while(T--)
{
scanf("%d%d%d%d%d",&n,&m,&a,&b,&c);
int i;
for(i=100;i>n;i--)
if(check(i))
break;
printf("%d\n",i);
}
return 0;
}
https://buaacoding.cn/problem/1910/index
看见异或第一反应一定要按位拆分,对于0起从低到高第i位,如果从x到y的边权的第i位中存在奇数个1,那么答案就要加上 x 到 y 边 权 和 ∗ 2 i x到y边权和*2^i x到y边权和∗2i。再考虑固定x为起点,所有的y的贡献用一次dfs可求,得 a n s x ans_x ansx,接着改变x,去O1更新 a n s x ans_x ansx,加入答案之中,也就是所谓的旋根操作。
为了实现方便,我采用了黑白染色法,经过此位为1的边则子点和父点不同,否则相同。以1为首个x,第一次对1染白色,统计所有黑点的贡献以及每个子树中黑点的个数,同样旋根的时候也只以白点为起点,看黑点的贡献,第一次对1染黑色,统计所有白点的贡献以及每个子树中白点的个数,同样旋根的时候也只以黑点为起点,看白点的贡献。首次dfs记为mark函数,旋根操作为rot函数。
不知道哪里写臭了,我卡常了,后来强硬地把mark函数改成非递归AC了,很不解。
#include
#include
#define bit(a,i) (a>>i&1)
#define mo 1000000007
using namespace std;
using LL=long long;
struct edge
{
int to;
unsigned w;
int nxt;
}E[200005];
struct item
{
int o,x,fa;
LL sum;
int j;
}S[100005];
int T,n,x,y,cnt[100005],first[100005],en,p,top;
LL res,f[100005],t1;
unsigned z;
void mark(int o, int x, int fa, LL sum)
{
top=0;
int j;
loop:
for(j=first[x];j;j=E[j].nxt)
if(E[j].to!=fa)
{
t1=sum+E[j].w;
if(bit(E[j].w,p)^o)
{
cnt[E[j].to]=1;
f[E[j].to]=t1;
}
S[top++]=(item){o,x,fa,sum,j};
o^=bit(E[j].w,p),fa=x,x=E[j].to,sum=t1;
goto loop;
loop2:
--top;
o=S[top].o,fa=S[top].fa,x=S[top].x,sum=S[top].sum,j=S[top].j;
}
for(j=first[x];j;j=E[j].nxt)
if(E[j].to!=fa)
cnt[x]+=cnt[E[j].to],f[x]=(f[x]+f[E[j].to])%mo;
if(top>0)
goto loop2;
}
void rot(int o, int x, int fa, LL &ans, LL tmp, int c)
{
if(c==o)
ans=(ans+tmp)%mo;
int j;
for(j=first[x];j;j=E[j].nxt)
if(E[j].to!=fa)
rot(o,E[j].to,x,ans,(tmp+(LL)(cnt[1]-2*cnt[E[j].to]+mo)*E[j].w%mo+mo)%mo,c^bit(E[j].w,p));
}
LL solve()
{
memset(f+1,0,n*sizeof(LL));
memset(cnt+1,0,n*sizeof(int));
mark(0,1,0,0);
LL ans=0;
rot(0,1,0,ans,f[1],0);
memset(f+1,0,n*sizeof(LL));
memset(cnt+1,0,n*sizeof(int));
mark(1,1,0,0);
cnt[1]++;
rot(1,1,0,ans,f[1],0);
return ans;
}
void read(int &x)
{
int f=1;x=0;char s=getchar();
while(s<'0'||s>'9'){if(s=='-')f=-1;s=getchar();}
while(s>='0'&&s<='9'){x=x*10+s-'0';s=getchar();}
x*=f;
}
void read(unsigned &x)
{
int f=1;x=0;char s=getchar();
while(s<'0'||s>'9'){if(s=='-')f=-1;s=getchar();}
while(s>='0'&&s<='9'){x=x*10+s-'0';s=getchar();}
x*=f;
}
int main()
{
read(T);
while(T--)
{
read(n);
memset(first+1,0,n*sizeof(int));
en=0;
for(int i=1;i<n;i++)
{
read(x),read(y),read(z);
E[++en]=(edge){y,z,first[x]};
first[x]=en;
E[++en]=(edge){x,z,first[y]};
first[y]=en;
}
res=0;
for(int i=0;i<32;i++)
{
p=i;
res=(res+solve()*(1LL<<i)%mo)%mo;
}
printf("%lld\n",res*(mo+1)/2%mo);
}
return 0;
}
https://buaacoding.cn/problem/1918/index
水题不解释了,写个低效的代码玩玩
#include
using namespace std;
int a[105],b[105],T,n;
bool check(int x)
{
int ret=x;
for(int i=1;i<=n;i++)
{
ret+=b[i];
if(ret<a[i])
return false;
}
return true;
}
int sum(int x)
{
int ret=x,res=0;
for(int i=1;i<=n;i++)
ret+=b[i],res+=ret;
return res;
}
int main()
{
scanf("%d",&T);
while(T--)
{
scanf("%d",&n);
for(int i=1;i<=n;i++)
scanf("%d",&a[i]);
for(int i=1;i<=n;i++)
scanf("%d",&b[i]);
int ans=0;
while(!check(ans))
++ans;
printf("%d\n",sum(ans));
}
return 0;
}
https://buaacoding.cn/problem/1894/index
考虑从大到小枚举gcd的值,找出所有gcd倍数的点,尝试连边,如果在同一个并查集里就不用连了,否则就连,加入并查集。其实就是克鲁斯卡尔算法的想法,效率的话是由一个数的因子个数均摊是logn的级别来限制住的(虽然只是均摊,关于一个数因子个数最多有多少个,没有固定的级别,但也不会远远超过logn)。
#include
#include
#include
using namespace std;
int T,n,fa[100005];
vector<int> h[100005],g;
long long ans;
int fis(int x)
{
return fa[x]==x?x:fa[x]=fis(fa[x]);
}
int main()
{
scanf("%d",&T);
while(T--)
{
scanf("%d",&n);
for(int i=1E5;i>0;i--)
h[i].clear();
for(int i=1,a;i<=n;i++)
scanf("%d",&a),h[a].push_back(i),fa[i]=i;
ans=0;
for(int i=1E5;i>0;i--)
{
for(int j=i;j<=1E5;j+=i)
for(int k:h[j])
g.push_back(k);
for(int j=0;j<g.size();j++)
if(fis(g[j])!=fis(g[0]))
fa[fis(g[j])]=fis(g[0]),
ans+=i;
g.clear();
}
printf("%lld\n",ans);
}
return 0;
}
https://buaacoding.cn/problem/1908/index
式子就抄一下官方题解
筛完质数之后,接下来的话我用dp数组记录前一个求积,xdp数组记录后一个求积。埃氏筛就够了,不用欧拉筛。
#include
#define mo 998244353
using namespace std;
using LL=long long;
const int maxn=1E7+5;
int n,T,dp[maxn],xdp[maxn],ans[maxn];
bool vis[maxn];
void sieve(int n)
{
for(int i=1;i<=n;i++)
xdp[i]=dp[i]=1;
for(int i=2;i*i<=n;i++)
if(!vis[i])
for(int j=i*i;j<=n;j+=i)
vis[j]=true;
LL j;
for(int i=2,t;i<=n;i++)
if(!vis[i])
{
for(j=i,t=1;j<=n;j*=i,t++);
j/=i,t--;
for(int h=j;h>=i;h/=i,t--)
for(int k=h;k<=n;k+=h)
if(k%((LL)h*i)!=0)
dp[k]*=t+1,xdp[k]=(LL)xdp[k]*((k-1)*t+1)%mo;
}
for(int i=1;i<=n;i++)
ans[i]=(ans[i-1]+(LL)xdp[i+1]*dp[i])%mo;
}
int main()
{
scanf("%d",&T);
sieve(1E7+1);
while(T--)
{
scanf("%d",&n);
printf("%d\n",ans[n]);
}
return 0;
}
https://buaacoding.cn/problem/1912/index
所有数据先离散化。首先明确一点,想维护k次方和,维护0~k-1次方和在所难免。第二,在求上升序列的时候,我们需要找到所有的前继点,用它们的数值和去更新这个节点的数值,这里意味着需要树状数组,所以开一个树状数组,节点是一个数组,第i位维护i次方和。更新的方式很容易推,是一个系数是杨辉三角的方程。
数字出现多次不可怕,更新时顺便乘上次数即可。
#include
#include
#define mo 1000000007
using namespace std;
using LL=long long;
pair<int,int> p[100005];
int a[100005],y[100005],T,n,k,m,C[22][22];
struct item
{
int d[22];
item operator + (item &t)
{
item res;
for(int i=0;i<=k;i++)
res.d[i]=(d[i]+t.d[i])%mo;
return res;
}
}tmp,ttmp;
struct Finwick
{
int n;
item C[100005];
void init(int x)
{
n=x;
for(int i=1;i<=n;i++)
for(int j=0;j<=k;j++)
C[i].d[j]=0;
}
item sum(int x)
{
item res;
for(int i=0;i<=k;i++)
res.d[i]=0;
while(x)
res=res+C[x],x-=x&-x;
return res;
}
void add(int x, item d)
{
while(x<=n)
C[x]=C[x]+d,x+=x&-x;
}
}F;
void C_init(int n)
{
C[0][0]=1;
for(int i=1;i<=n;i++)
for(int j=0;j<=i;j++)
C[i][j]=C[i-1][j]+(j>0?C[i-1][j-1]:0);
}
int main()
{
scanf("%d",&T);
C_init(20);
while(T--)
{
scanf("%d%d",&n,&k);
for(int i=1;i<=n;i++)
scanf("%d%d",&p[i].first,&y[i]),p[i].second=i;
sort(p+1,p+n+1);
m=1;
for(int i=1;i<=n;i++)
{
a[p[i].second]=m;
if(i<n&&p[i].first!=p[i+1].first)
m++;
}
F.init(m);
for(int i=1;i<=n;i++)
{
ttmp=F.sum(a[i]-1);
for(int j=0;j<=k;j++)
{
tmp.d[j]=ttmp.d[j]+1;
for(int h=0;h<j;h++)
tmp.d[j]=(tmp.d[j]+(LL)C[j][j-h]*ttmp.d[h])%mo;
}
for(int j=0;j<=k;j++)
tmp.d[j]=(LL)tmp.d[j]*y[i]%mo;
F.add(a[i],tmp);
}
ttmp=F.sum(m);
printf("%d\n",ttmp.d[k]);
}
return 0;
}
https://buaacoding.cn/problem/1916/index
枚举题,枚举有可能抽到的两张牌,check是否组成顺子。没有坑点,不要粗心即可。
#include
#include
#include
using namespace std;
int T,pos[20];
char s[10];
map<char,int> M;
bool check()
{
for(int i=1;i<=9;i++)
if(pos[i]&&pos[i+1]&&pos[i+2]&&pos[i+3]&&pos[i+4])
return true;
if(pos[10]&&pos[11]&&pos[12]&&pos[13]&&pos[1])
return true;
return false;
}
int main()
{
M['A']=1;
for(int i=2;i<10;i++)
M[i+'0']=i;
M['T']=10;
M['J']=11;
M['Q']=12;
M['K']=13;
scanf("%d",&T);
while(T--)
{
scanf("%s",s);
memset(pos,0,sizeof(pos));
for(int i=0;i<5;i++)
pos[M[s[i]]]++;
int ans=0;
for(int i=1;i<=13;i++)
for(int j=1;j<=13;j++)
{
pos[i]++;
pos[j]++;
if(pos[i]<=4&&pos[j]<=4)
if(check())
if(i!=j)
ans+=(5-pos[i])*(5-pos[j]);
else
ans+=(6-pos[i])*(5-pos[i]);
pos[i]--;
pos[j]--;
}
printf("%d\n",ans/2);
}
return 0;
}
https://buaacoding.cn/problem/1898/index
注意到这些矩阵的平方是单位阵,这意味着运算存在异或性,多次相乘同一矩阵,相当于乘模2次。再观察到某个元素第i步更新影响的元素排布满足异或杨辉三角。
0:1
1:11
2:101
3:1111
4:10001
5:110011
6:1010101
7:11111111
由于第 2 i 2^i 2i行只涉及到2个元素,这意味着如果步数是2的幂,那么答案可以迅速求出。对任意步数,可以将步数k按位拆分,分logn次求即可。
#include
#include
#define bit(a,i) (a>>i&1)
using namespace std;
char s[2][1000005];
int T,n,k,o,p[128][128];
int main()
{
scanf("%d",&T);
p['A']['A']='A';
p['A']['T']=p['T']['A']='T';
p['A']['C']=p['C']['A']='C';
p['A']['G']=p['G']['A']='G';
p['C']['C']='A';
p['C']['G']=p['G']['C']='T';
p['C']['T']=p['T']['C']='G';
p['G']['T']=p['T']['G']='C';
p['G']['G']='A';
p['T']['T']='A';
while(T--)
{
scanf("%d%d%s",&n,&k,s[0]);
o=0;
for(int i=0;i<31;i++)
if(bit(k,i))
{
for(int j=0;j<n;j++)
s[o^1][j]=p[s[o][j]][s[o][(j+(1<<i))%n]];
o^=1;
}
s[o][n]='\0';
puts(s[o]);
}
return 0;
}
https://buaacoding.cn/problem/1896/index
解出这题全靠数院的队友。首先用容斥原理,求出违反超过第 k 1 , k 2 , k 3 … k1,k2,k3… k1,k2,k3…个小弟总数的方案(这里就是相当于转化为无上限的分割问题,可以用插板原理变成组合数),然后加加减减得到答案。
大数组合数需要用到卢卡斯定理,现在还没完全领悟,网上抄的模板。然而还没完,模数是合数,需要用中国剩余定理对质因子模数答案进行组合。注意中国剩余定理过程中乘法数据范围可能超过long long,需要使用快速模乘黑科技。
#include
#include
using namespace std;
using LL=long long;
int n,T,k;
LL mo,f[20],m,p[20],q[20];
inline LL mul(LL a, LL b)
{
return (a%mo)*(b%mo)%mo;
}
inline LL quick_mul(LL p, LL q, LL mo)
{
p=(p%mo+mo)%mo,q=(q%mo+mo)%mo;
return ((p*q-(LL)((long double)p/mo*q+1E-6)*mo)%mo+mo)%mo;
}
LL quick_power(LL a, LL b)
{
LL res=1,base=a;
while(b)
{
if(b&1)
res=mul(res,base);
base=mul(base,base);
b>>=1;
}
return res;
}
LL C(LL a, LL b)
{
if(a<b)
return 0;
b=min(a-b,b);
LL u=1,d=1;
for(LL i=0;i<b;i++)
{
u=mul(u,a-i);
d=mul(d,i+1);
}
return mul(u,quick_power(d,mo-2));
}
LL lucas(LL a, LL b)
{
return !b?1:mul(C(a%mo,b%mo),lucas(a/mo,b/mo));
}
LL solve()
{
LL ans=0;
for(int i=0;i<(1<<n);i++)
{
LL t=1,sum=m;
for(int j=1;j<=n;j++)
if(i>>j-1&1)
sum-=f[j]+1,t*=-1;
if(sum<0)
continue;
ans=(ans+t*lucas(sum+n-1,n-1)+mo)%mo;
}
return ans;
}
void gcd(LL a, LL b, LL &d, LL &x, LL &y)
{
if(!b)
d=a,x=1,y=0;
else
gcd(b,a%b,d,y,x),y-=x*(a/b);
}
LL china(LL n, LL *a, LL *m)
{
LL M=1,d,y,x=0;
for(int i=0;i<n;i++)
M*=m[i];
for(int i=0;i<n;i++)
{
LL w=M/m[i];
gcd(m[i],w,d,d,y);
x=(x+quick_mul(y*w,a[i],M))%M;
}
return (x+M)%M;
}
int main()
{
scanf("%d",&T);
while(T--)
{
scanf("%d%lld%d",&n,&m,&k);
for(int i=1;i<=n;i++)
scanf("%lld",&f[i]);
for(int i=1;i<=k;i++)
{
scanf("%lld",&p[i]);
mo=p[i];
q[i]=solve();
}
printf("%lld\n",china(k,q+1,p+1));
}
return 0;
}
https://buaacoding.cn/problem/1911/index
官方题解的链表/并查集是一个更优秀的选择,也更容易理解。这里给出线段树解法。
一个数对比不比它大的数取模之后大小至少减到一半。所以在变0前,即使我们对每个数暴力修改,直到,效率依旧不会超过 n ∗ l o g M ∗ F n*logM*F n∗logM∗F M表示数据大小,F是均摊的单次修改效率,此处不会超过logn,区间足够大时约为1。用线段树维护最小值以及区间和。另外注意到如果一个区间的数都是0不应该被修改,所以要记录一下区间最大值,否则会有大量无效的修改,使得效率难以得到保证。
这么处理效率是有保证的,具体证明可以尝试绘画线段树结构图。
#include
#include
#define kl (k<<1)
#define kr (k<<1|1)
#define M (L+R>>1)
#define lin L,M
#define rin M+1,R
using namespace std;
using LL=long long;
struct node
{
LL x,y,sum;
};
int t,n,q,l,r;
node T[1<<19];
LL mv;
void maintain(int k)
{
T[k].x=min(T[kl].x,T[kr].x);
T[k].y=max(T[kl].y,T[kr].y);
T[k].sum=T[kl].sum+T[kr].sum;
}
void build_tree(int k, int L, int R)
{
if(L==R)
{
scanf("%lld",&T[k].sum);
T[k].x=T[k].y=T[k].sum;
if(T[k].x==0)
T[k].x=2E18;
return ;
}
build_tree(kl,lin);
build_tree(kr,rin);
maintain(k);
}
LL query(int k, int L, int R)
{
if(l<=L&&R<=r)
return T[k].sum;
LL res=0;
if(l<=M)
res+=query(kl,lin);
if(r>M)
res+=query(kr,rin);
return res;
}
LL query_min(int k, int L, int R)
{
if(l<=L&&R<=r)
return T[k].x;
LL res=2E18;
if(l<=M)
res=min(res,query_min(kl,lin));
if(r>M)
res=min(res,query_min(kr,rin));
return res;
}
void modify(int k, int L, int R)
{
if(T[k].y<mv)
return ;
if(L==R)
{
T[k].sum=T[k].x=T[k].y=T[k].sum%mv;
if(T[k].x==0)
T[k].x=2E18;
return ;
}
if(l<=M)
modify(kl,lin);
if(r>M)
modify(kr,rin);
maintain(k);
}
int main()
{
scanf("%d",&t);
while(t--)
{
scanf("%d%d",&n,&q);
build_tree(1,1,n);
while(q--)
{
scanf("%d%d",&l,&r);
printf("%lld ",query(1,1,n));
mv=query_min(1,1,n);
if(mv>0)
modify(1,1,n);
printf("%lld\n",query(1,1,n));
}
}
return 0;
}
https://buaacoding.cn/problem/1895/index
很容易发现固定轮数时,可以尽量用后面的棋子。于是想到二分答案。没什么大的坑点。我居然是因为变量读入顺序弄错导致WA了多次……
#include
#include
#include
#include
#define M (L+R>>1)
using namespace std;
using LL=long long;
int n,m,k,p,l,h[200005],sz,tmp[200005],tag[200005],T;
vector<int> V[200005];
bool check(int x)
{
memset(tmp,0,sizeof(tmp));
memset(tag,0,sizeof(tag));
int tsz=0;
for(int i=x;i>0;i--)
for(int j=0;j<m;j++)
{
tmp[V[i][j]]++;
if(tmp[V[i][j]]<=h[V[i][j]])
tag[i]++;
if(tmp[V[i][j]]==h[V[i][j]])
tsz++;
}
if(tsz<sz)
return false;
LL money=0;
for(int i=1;i<=x;i++)
{
money+=p+money/k;
money=min(money,(LL)(1E18));
money-=tag[i];
if(money<0)
return false;
}
return true;
}
int main()
{
scanf("%d",&T);
while(T--)
{
scanf("%d%d%d%d%d",&n,&m,&p,&k,&l);
for(int i=1;i<=n;i++)
{
V[i].clear();
for(int j=1,a;j<=m;j++)
scanf("%d",&a),V[i].push_back(a);
}
memset(h,0,sizeof(h));
sz=0;
for(int i=1,p;i<=l;i++)
{
scanf("%d",&p);
if(++h[p]==1)
++sz;
}
int L=1,R=n+1;
while(L<R)
if(!check(M))
L=M+1;
else
R=M;
printf("%d\n",L>n?-1:L);
}
return 0;
}