国庆集训
简称国集
别问我 Day1 和 Day2 哪儿去了
Day3
说实话今天做题时感觉就像昨天磕了药或者今天没吃药一样
T1
题目描述
题目描述
小 A 和小 B 都是 51nod 的用户。有一天小 A 打开了程序挑战排行榜,发现小B只要刷至少一道题并且获得至少 \(S\)(\(S\) 有可能是负数)分就能超过小 A 了。小 A 为了维护自己的排名,迅速展开了行动。
具体而言,这里假设 51nod 上的题目排成了一个序列,每道题的得分有正有负。小 B 一天可以把任意一个区间内的所有问题解决掉,并获得这区间内所有问题的得分。而小 A 可以删去序列中的若干问题。但是为了不被人发现,小 A 想知道在这天内为了维护自己的排名最少需要删去多少个问题?
输入格式
第一行一个正整数 \(n\),表示问题的数目。
第二行一个整数 \(S\),表示小 B 至少获得 \(S\) 分在的得分上就会超过小 A。
第三行 \(n\) 个整数 \(v_i\),表示每道题的分值。
\(n\le 10^6,\|S\|\le 10^{15},\|vi\|\le 10^9\)
输出格式
一个整数,表示问题的答案。
数据范围
对于 \(2\%\) 的数据,\(1\le n\le 10\);
对于 \(16\%\)的数据,\(1\le n\le 2000\);
对于 \(100\%\)的数据,\(1\le n\le 10^6,\|S\|\le 10^{15},\|v_i\|\le 10^9\)。
Solution
-
题意:从一个数列中删去一些数,使得剩下的序列中的任意连续区间中数的和都不大于给定的 \(S\) (如果区间中没有数了也符合条件);
-
首先我们知道,当 \(S\) 和 \(v_i\) 全为正时,我们可以用单调队列和 \(\mathtt{DP}\) 的思想来完成这道题。
-
分成两类,考虑讨论 \(S\) 的正负:
- 当 \(S\) 为正数时,如果 \(v_i\) 为正,则用单调队列跑,如果当前 \(sum\ge S\) 则不断弹出队列中的最大值;如果 \(v_i\) 为负,则将队列中的最小值弹出来抵消这个负数:如果单调队列中已经没有元素了,而还有负数没有抵消完,那就把当前统计的和与存负数的队列全部清空;
- 当 \(S\) 为负数时,直接统计有多少数 \(\ge S\) 即可;
-
由于这个优先队列既要求最大值又要求最小值,所以我使用了 \(\mathtt{multiset}\) 来处理;
AC code
#include<bits/stdc++.h>
using namespace std;
#define ll long long
inline ll read(){
ll s=0,f=1;
char ch=getchar();
while(!isdigit(ch)){
if(ch=='-') f=-1;
ch=getchar();
}
while(isdigit(ch)){
s=s*10+int(ch-'0');
ch=getchar();
}
return s*f;
}
const int N=1e6+10;
int n;
ll S;
int v[N];
int ans=0;
#define pnter multiset<int>::iterator
#define re register
multiset<int>s;
queue<int>q;
int main(){
// freopen("score.in","r",stdin);
n=read(),S=read();
for(int i=1;i<=n;++i)
v[i]=read();
ll sum=0;
if(S>0){
for(re int i=1;i<=n;++i){
// cerr<<i<<endl;
if(v[i]<0){
q.push(v[i]);
sum+=v[i];
}
else{
sum+=v[i];
s.insert(v[i]);
}
while(q.size()){
int x=q.front();
q.pop();
while(x<0 && s.size()){
pnter it=s.begin();
x+=*it;
s.erase(it);
}
if(x>0) s.insert(x);
if(!s.size()){
q.push(x);
break;
}
}
if(!s.size() && q.size()){
sum=0;
while(q.size())
q.pop();
}
while(sum>=S && s.size()){
pnter it=s.end();
--it;
sum-=*it;
s.erase(it);
++ans;
}
}
// cerr<<q.size()<<endl;
}
else{
for(re int i=1;i<=n;++i)
if(v[i]>=S)
++ans;
}
printf("%d",ans);
return 0;
}
T2
题目描述
题目描述
松鼠大作战这个游戏的完整地图,可以看做是一棵 \(n\) 个节点的树,树根为 1,节点编号 1 到 \(n\)。
每次任务可以抽象为从一个关卡 \(u\) 开始,不断向上打怪升级(只能向上走),一直到关卡 \(v\) 为止。(保证 \(v\) 是 \(u\) 的祖先)。
在一次任务开始的时候,你会得到一个武器,攻击力为 \(c\),然后每一关都有一个武器宝箱,宝箱中的武器有一个攻击力 \(a_i\)。如果箱子中武器的攻击力高于你手中的,你就会选择用这个武器替换手中的武器。
这样的任务总共有 \(q\) 个。给出每一个关卡中武器的攻击力,以及 \(q\) 次任务的起点与终点,以及任务初始时的武器攻击力。
现在想知道,对于每个任务,你会更换多少次武器。
输入格式
第一行,两个正整数 \(n,q\)(\(2\le n\le 10^5,1\le q\le 10^5\))。
第二行,\(n\) 个正整数 \(a_i\) 描述每个关卡上武器的攻击力。
接下来 \(n-1\) 行,每行描述一条道路 \(x,y(1\le x,y\le n)\),表示有一条连接 \(x\) 和 \(y\) 的道路。
接下来 \(q\) 行,每行描述一次行程 \(u,v,c(1\le u,v\le n)\)。
输出格式
输出共 \(q\) 行,对于每次任务输出一行表示更换武器的次数。
数据范围
对于 \(10\%\) 的数据,\(2\le n\le 100,1\le q\le 100\);
对于 \(22.5\%\) 的数据,\(2\le n\le 5000,1\le q\le 2000\);
对于 \(100\%\) 的数据,\(2\le n\le 10^5,1\le q\le 10^5,1\le a_i\le 10^5,1\le c\le 10^5\)。
Solution
-
容易想到一个结论,就是在不考虑终点和初始武器值的情况下,同一条到根的路径上会有许多起点的换武器方案后半部分相同。
-
考虑倍增。建树后从上往下 \(\mathtt{DFS}\) 时处理出每个节点的深度和倍增的表:
对于一个新的节点,判断如果 \(a_{它的父亲}>a_{它自己}\),则倍增表中第一个转移就确定了;否则用一个函数以它父亲的倍增表向上寻找第一个 \(a\) 值大于它的点,然后开始转移。
由于题目确保起点和终点在同一条路径上,所以可以直接把终点转化为深度。
起点先判断要不要原地换武器,如果要,那么直接以这个点为起点进行倍增;否则找到第一个 \(a\) 值大于它的点,再开始进行倍增;
AC code
#include<bits/stdc++.h>
using namespace std;
inline int read(){
int s=0,f=1;
char ch=getchar();
while(!isdigit(ch)){
if(ch=='-') f=-1;
ch=getchar();
}
while(isdigit(ch)){
s=s*10+int(ch-'0');
ch=getchar();
}
return s*f;
}
const int N=1e5+10;
int n,q;
int a[N];
int head[N],ver[N<<1],nxt[N<<1],tot=0;
int nex[25][N],t=20;
int dep[N],f[N];
void add(int x,int y){
ver[++tot]=y,nxt[tot]=head[x],head[x]=tot;
return ;
}
int check(int p,int x){
if(!p) return 0;
int pos=p;
for(int i=t;i>-1;--i){
if(!nex[i][pos]) continue;
if(a[nex[i][pos]]<=x) pos=nex[i][pos];
}
for(int i=0;i<=t;++i){
if(!nex[i][pos]) break;
if(a[nex[i][pos]]>x){
pos=nex[i][pos];
break;
}
}
return (pos==p || a[pos]<=x)?0:pos;
}
void dfs(int p,int fa){
dep[p]=dep[fa]+1;
f[p]=fa;
if(a[fa]>a[p])
nex[0][p]=fa;
else nex[0][p]=check(f[p],a[p]);
for(int i=1;i<=t && nex[i-1][p];++i)
nex[i][p]=nex[i-1][nex[i-1][p]];
for(int i=head[p];i;i=nxt[i]){
if(ver[i]==fa) continue;
dfs(ver[i],p);
}
return ;
}
int find(int p,int top,int c){
// if(!p) return 0;
if(!nex[0][p]) return int(a[p]>c);
if(dep[nex[0][p]]<top) return int(a[p]>c);
int pos=p,cnt=1;
if(a[pos]<=c)
pos=check(p,c);
if(dep[pos]<top || !pos || a[pos]<=c) return 0;
for(int i=t;i>-1;--i){
if(!nex[i][pos]) continue;
if(dep[nex[i][pos]]>=top){
cnt+=(1<<i);
pos=nex[i][pos];
}
}
return (!pos || a[pos]<=c)?0:cnt;
}
int main(){
// freopen("squirrel.in","r",stdin);
// freopen("squirrel.ans","w",stdout);
n=read(),q=read();
for(int i=1;i<=n;++i)
a[i]=read();
int x,y,z;
for(int i=1;i<n;++i){
x=read(),y=read();
add(x,y);
add(y,x);
}
dep[0]=0;
dfs(1,0);
while(q--){
x=read(),y=read(),z=read();
y=dep[y];
printf("%d\n",find(x,y,z));
}
return 0;
}
Day4
T1
题目描述
题目背景
最近白云和他的同伴白兔又举行了一场比赛。他们准备用下棋来一决胜负。
题目描述
下棋规则是这样的:棋盘是个长度为 \(n\) 的序列,起初在第 \(a\) 个位置和第 \(b\) 个位置分别放了一个棋子 \((a\neq b)\)。
接下来两人轮流操作,白云为先手,每轮寻找一个合法空格子下一个棋,最后无法下棋的人失败。对于每轮,某个空格子k合法当且仅当存在两个位置 \(i, j\)\((i\neq j)\),两个位置都有棋子, 且 \(k=i+j\or\ k=|i−j|\),
他们想知道在这样的情况下谁能获胜。
对于前 \(30\%\) 的数据,满足 \(T\le 10,n≤10\).
对于 \(100\%\) 的数据,满足 \(T\le 100,n≤10^{18}\).
输入格式
第一行一个整数 \(T\) 表示,数据组数。
接下来 \(T\) 行,每行 3 个整数 \(n, a, b\)。
输出格式
输出共 \(T\) 行。对于每组数据,若白云取胜,输出“baiyun”,否则输出“baitu”(不包括引号)。
Solution
- 怎么说呢,这题和之前的虚空起高楼不能说是毫无相似之处,只能说是一模一样……
AC code
#include<bits/stdc++.h>
using namespace std;
#define ll long long
int T;
ll n,a,b;
ll gcd(ll a,ll b){
return (!a)?b:gcd(b%a,a);
}
int main(){
// freopen("game.in","r",stdin);
// freopen("game.out","w",stdout);
scanf("%d",&T);
while(T--){
scanf("%lld%lld%lld",&n,&a,&b);
ll t=gcd(a,b);
ll ans=n/t-2;
if(ans<=0 || ans%2==0)
printf("baitu\n");
else printf("baiyun\n");
}
return 0;
}
T2
- 这题我仿佛过了,又仿佛寄了……
Day5
- 要不是考场上没想起来这 T2 怎么优化,这套题岂不是 \(\mathtt{255pts}\) 起步……
T1.签到
题目描述
题目描述
小 Q 和小王在博弈。
对于所有 \(1\le i\le n\),桌子上有 \(a_i\) 个数字 \(i\)。
小 Q 和小王轮流操作,小 Q 先手。每次操作可以选任意两个相等的数 \(x\),从桌子上拿走这两个数,并再放入一个 \(x+1\)。
无法操作的人就输了。如果两人都采取最优策略,问谁会赢。
一个测试点含有多组数据。
输入格式
第一行一个整数 \(T\),表示数据组数。
接下来 \(T\) 组每组第一行一个整数 \(n\),第二行 \(n\) 个整数表示 \(a_1,a_2,...a_n\)。
输出格式
\(T\) 行,每行一个大写字母表示该组测试的答案。若小 Q 会赢输出 Q
,否则输出 W
。
数据范围
对于 \(30\%\) 的数据,满足 \(T\le 10,n\le 3,a_i\le 5\)。
对于所有数据,满足 \(T\le 10,n\le 10^5,0\le a_i\le 10^9\)。
题目名字已经揭示了它的难度
Solution
- 由于可以想到,这个游戏的结束局面可能也仅可能是当数列中的数全部都只剩一个或者没有,所以从小到大把所有能换掉的全部换掉,直到没有数能够被消掉为止。线性统计消掉所有数总共有多少次操作即可。
AC code
#include<bits/stdc++.h>
using namespace std;
inline int read(){
int s=0,f=1;
char ch=getchar();
while(!isdigit(ch)){
if(ch=='-') f=-1;
ch=getchar();
}
while(isdigit(ch)){
s=s*10+int(ch-'0');
ch=getchar();
}
return s*f;
}
#define ll long long
const int N=1e5+10;
int T,n;
ll ans=0;
ll a,last;
//ll a[N];
int main(){
// freopen("a.in","r",stdin);
// freopen("a.out","w",stdout);
T=read();
while(T--){
n=read();
a=ans=last=0;
for(int i=1;i<=n;++i){
a=read();
a+=0ll+last;
a>>=1;
ans+=a;
last=a;
}
while(last){
ans+=(last>>1);
last>>=1;
}
if(ans%2) putchar('Q');
else putchar('W');
putchar('\n');
}
return 0;
}
T2.DS
题目描述
(待补)
Solution
-
用线段树或者分块暴搞就行了。由于每个数被取余 \(\log\) 次之后基本都为 0 或 1,所以分块搞可以优化一部分;
-
考场上写了个 \(\mathtt{pushdown()}\) 巨繁琐的假线段树。因为对于这道题来说取余操作不具有什么好的性质,所以取余之间没办法消除,只能暴力。
线段树维护区间最大值。写两个修改函数,一个支持单点修改,一个支持区间取余。对于区间取余操作要注意 \(\mathtt{pushdown()}\) 操作必须全部下传,否则 \(max\) 的统计很可能会出错。
但如果每次都全部下传那这个线段树的时间复杂度会逼近 \(\mathtt{O(n^2\log n)}\),所以在本来巨慢的\(\mathtt{pushdown()}\) 中加入一个优化:if(tr[p].tg>tr[p].mx){ tr[p].tg=0; return ; }
即可避免过多无意义的下传。
如果不加这个优化你只能得暴力分
AC code
#include<bits/stdc++.h>
using namespace std;
inline int read(){
int s=0,f=1;
char ch=getchar();
while(!isdigit(ch)){
if(ch=='-') f=-1;
ch=getchar();
}
while(isdigit(ch)){
s=s*10+int(ch-'0');
ch=getchar();
}
return s*f;
}
inline void write(int x){
if(!x) return ;
write(x/10);
putchar(char('0'+x%10));
return ;
}
#define re register
const int N=1e5+10;
int n,m;
int a[N];
struct memr{
int l,r;
int mx,tg;
}tr[N<<3];
void pushup(int p){
tr[p].mx=max(tr[p<<1].mx,tr[p<<1|1].mx);
return ;
}
void pushdown(int p){
if(tr[p].l==tr[p].r) return ;
if(tr[p].tg){
pushdown(p<<1),pushdown(p<<1|1);
pushup(p);
if(tr[p].tg>tr[p].mx){
tr[p].tg=0;
return ;
}
tr[p<<1].mx%=tr[p].tg;
tr[p<<1|1].mx%=tr[p].tg;
tr[p<<1].tg=tr[p].tg;
tr[p<<1|1].tg=tr[p].tg;
tr[p].tg=0;
pushdown(p<<1),pushdown(p<<1|1);
}
pushup(p);
return ;
}
void change(int p,int l,int r,int d){
if(d>tr[p].mx) return ;
pushdown(p);
if(l<=tr[p].l && tr[p].r<=r){
tr[p].tg=d;
tr[p].mx%=d;
pushdown(p);
return ;
}
int mid=(tr[p].l+tr[p].r)>>1;
if(l<=mid) change(p<<1,l,r,d);
if(mid<r) change(p<<1|1,l,r,d);
pushup(p);
return ;
}
void mix(int p,int x,int v){
if(x<=tr[p].l && tr[p].r<=x){
tr[p].mx=v;
return ;
}
pushdown(p);
int mid=(tr[p].l+tr[p].r)>>1;
if(x<=mid) mix(p<<1,x,v);
else mix(p<<1|1,x,v);
pushup(p);
return ;
}
int ask(int p,int l,int r){
if(l<=tr[p].l && tr[p].r<=r)
return tr[p].mx;
int cnt=-1;
pushdown(p);
int mid=(tr[p].l+tr[p].r)>>1;
if(l<=mid) cnt=max(cnt,ask(p<<1,l,r));
if(mid<r) cnt=max(cnt,ask(p<<1|1,l,r));
pushup(p);
return cnt;
}
void build(int p,int l,int r){
tr[p].l=l,tr[p].r=r;
tr[p].tg=tr[p].mx=0;
if(l==r){
tr[p].mx=a[l];
return ;
}
int mid=(l+r)>>1;
build(p<<1,l,mid);
build(p<<1|1,mid+1,r);
pushup(p);
return ;
}
int main(){
// freopen("b10.in","r",stdin);
// freopen("b.out","w",stdout);
n=read(),m=read();
for(re int i=1;i<=n;++i)
a[i]=read();
build(1,1,n);
re int opt,x,y,z;
while(m--){
opt=read(),x=read(),y=read();
if(opt==2){
z=read();
change(1,x,y,z);
}
else{
if(opt==1) mix(1,x,y);
else printf("%d\n",ask(1,x,y));
}
}
return 0;
}
T3.计数
- 一看题面,啊又是一道美妙的数论题
但是推不来公式。
题面描述
待补
Solution
- 考虑不同情况下逆序对产生的情况。
AC code
#include<bits/stdc++.h>
#define I using
#define AK namespace
#define IOI std
I AK IOI;
inline int read(){
int s=0,f=1;
char ch=getchar();
while(!isdigit(ch)){
if(ch=='-') f=-1;
ch=getchar();
}
while(isdigit(ch)){
s=s*10+int(ch-'0');
ch=getchar();
}
return s*f;
}
const int mod=998244353;
const int N=1e7;
#define ll unsigned long long
#define re register
int T,n;
ll d[N+10];
ll inv2,inv6;
inline ll ksm(int x,int y){
re ll cnt=1,dx=x;
for(;y;y>>=1,(dx*=dx)%=mod)
if(y&1)
(cnt*=dx)%=mod;
return cnt;
}
void pre(){
inv2=ksm(2,mod-2),inv6=ksm(6,mod-2);
d[2]=1;
for(re int i=3;i<=N;++i)
d[i]=1ll*(i-1)*(d[i-1]+d[i-2])%mod;
return ;
}
int main(){
// freopen("c.in","r",stdin);
// freopen("c.out","w",stdout);
pre();
T=read();
while(T--){
n=read();
if(n==2){
printf("%d\n",1);
continue;
}
ll cnt=1ll*(d[n]+d[n-2])*inv2%mod*n%mod*(n-1)%mod*inv2%mod;
ll x=1ll*n*(n-1)%mod*(n-2)%mod*inv6%mod;
printf("%lld\n",(cnt+1ll*x*d[n-1]%mod*ksm(n-2,mod-2)%mod)%mod);
}
return 0;
}
T4.王唱
题目描述
待补
- 诈骗题……
Solution
-
虽然看上去是一道非常***钻的字符串科技题,但其实几乎不用字符串知识……
-
使用 \(\mathtt{bitset}\),对于每一个出现过的字符存储它在原串中的出现情况;如果遇到修改,就在原串和所包含的字符对应的 \(\mathtt{bitset}\) 中暴力修改即可;遇到查询,将查询字符串中的所有字符的 \(bitset\) 通过位移挪到相对相同的位置取 \(\&\),再通过位移删掉区间以外的即可;
AC code
#include<bits/stdc++.h>
using namespace std;
inline int read(){
int s=0,f=1;
char ch=getchar();
while(!isdigit(ch)){
if(ch=='-') f=-1;
ch=getchar();
}
while(isdigit(ch)){
s=s*10+int(ch-'0');
ch=getchar();
}
return s*f;
}
const int N=1e5+1;
#define re register
bitset<N>b[260],ans;
map<char,int>m;
int n,k,cnt=0;
string s;
int main(){
// freopen("d.in","r",stdin);
// freopen("d.out","w",stdout);
n=read(),k=read();
cin>>s;
--n;
for(re int i=0;i<=n;++i)
b[int(s[i])].set(i);
int opt,x,y,l;
string t;
while(k--){
opt=read(),x=read(),y=read();
--x,--y;
cin>>t;
if(opt==1){
l=t.size();
for(re int i=x;i<=y;++i){
b[int(s[i])].reset(i);
b[int(t[i-x])].set(i);
s[i]=t[i-x];
}
}
else{
ans=b[int(t[0])];
l=t.size();
for(re int i=1;i<l;++i)
ans&=(b[int(t[i])]>>i);
// ans>>=x;
// ans<<=(N+x+l-2-y);
printf("%d\n",((ans>>=x)<<=(N+x+l-2-y)).count());
}
}
return 0;
}