官方讲解(这场讲的很不错,推荐一听,尤其是G题,有东西的)
这场难度比较大,C是一个博弈,D是一个神秘数学题,E题需要手玩一下会发现一些性质,猜结论,F题二分写法简单,但是可能不太直观,或者套主席树板子,G题是个差分约束,很难。
A 会赢吗?
思路:
比较一下生命和受到伤害的大小即可。本来担心出题人卡浮点数精度,然后看到才到小数点后五位,应该不是很好卡,所以用浮点也能过。
code:
#include <iostream>
#include <cstdio>
using namespace std;
double a,b;
int main(){
cin>>a>>b;
puts(a<b?"YES":"NO");
return 0;
}
B 能做到的吧
思路:
贪心,如果后面有一个数比前面(高位上最小的数)大,那就交换,这样得到的数一定更大。
code:
#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
int T;
string s;
int main(){
cin>>T;
while(T--){
cin>>s;
bool flag=false;
for(int i=1,mi=s[0]-'0';i<s.length();i++){
if(s[i]-'0'<=mi)mi=s[i]-'0';
else {
flag=true;
break;
}
}
puts((flag)?"YES":"NO");
}
return 0;
}
C 会赢的!
思路:
一个简单博弈。
首先如果终点不在第一象限,那么一定是平局,因为两个人都不可能走得到终点。
其次如果终点的横纵坐标之和如果是奇数,那么一定是阿龙赢或者是平局;否则如果终点的横纵坐标之和如果是偶数,那么一定是小歪赢或者是平局。因为每轮操作给横纵坐标某一个加一,因此横纵坐标之和就是进行的轮数,奇数轮是阿龙走的,这时候走到终点当然一定是阿龙赢,否则就是小歪赢。
然后如果 x x x 和 y y y 的差值超过 1 1 1,那么一定是平局,否则某个人赢。因为当 x x x 和 y y y 的差值超过 1 1 1 时,假如 x > y x>y x>y,赢不了的那个人一定会往 y y y 方向走,这时另一个人就算往 x x x 方向往终点拉,也拉不回去,然后与终点擦肩而过。反之,两个人互相拉扯,如果输的人走 x x x,赢的人就走 y y y,这时一定可以保证 x , y x,y x,y 的值保持一样,可能能赢的人就能赢了。
code:
#include <iostream>
#include <cstdio>
using namespace std;
int T,x,y;
int main(){
cin>>T;
while(T--){
cin>>x>>y;
if(x<0 || y<0)puts("PING");
else if(abs(x-y)>1)puts("PING");
else if((x+y)&1)puts("YES");
else puts("NO");
}
return 0;
}
D 好好好数
思路:
可以发现一个 k − k- k− 好数写成 k k k 进制的话每一位都只能是 0 0 0 或 1 1 1。给你一个 n n n ,写成 k k k 进制的话数位则有可能出现多个数。现在让你用最少的 k − k- k− 好数加起来等于 n n n,那么如果 n n n 在 k k k 进制下,所有的数位上的最大数字为 x x x,那么最少就需要 x x x 个 k − k- k− 好数来凑。
不过需要特判 1 − 1- 1− 好数的情况。
code:
#include <iostream>
#include <cstdio>
using namespace std;
typedef long long ll;
int T;
ll n,k;
int main(){
cin>>T;
while(T--){
cin>>n>>k;
ll mx=0;
if(k==1){
cout<<1<<endl;
continue;
}
while(n){
mx=max(mx,n%k);
n/=k;
}
cout<<mx<<endl;
}
return 0;
}
E 好好好数组
思路:
其实手玩一会的话,会发现只有三种可能。
首先第一个数一定是 0 0 0,因为第二个数要模 1 1 1 得到第一个数,那第二个数无论是什么第一个数都肯定是 0 0 0。
第二个数可以是 0 0 0 或 1 1 1,不能大于等于 2 2 2,因为第三个数要模 2 2 2。同理第 i i i 个数也最多只能是 0 ∼ i − 1 0\sim i-1 0∼i−1。如果第二个数是 0 0 0 的话,第三个数可以接着是 0 0 0,也可以取 1 , 2 1,2 1,2。同理接着向下。所以一种可能的构造方法就是全都取 0 0 0,形如 0 0 0 0 0 0 … 0\ 0\ 0\ 0\ 0\ 0\dots 0 0 0 0 0 0…。
如果第二个数取 1 1 1,那么后面的数可以接着取 1 1 1,但是却不能取 i i i 也就是 3 3 3( 3 % 2 = 1 3\%2=1 3%2=1),否则第四个数无论怎么取,模 3 3 3 都不可能等于 3 3 3。
多试几次发现可以变一次数变成 0 0 2 2 2 2 0\ 0\ 2\ 2\ 2\ 2 0 0 2 2 2 2 这种前面一部分是一个数,后面一部分是另一个数。但是不能变两次数变成 0 0 2 2 6 … 0\ 0\ 2\ 2\ 6\dots 0 0 2 2 6… 这种,因为第二次变完之后,这个数就太大了(甚至可能超过 n n n),后一个数 i i i 无论怎么变,模上 i − 1 i-1 i−1 都不能得到一个大于等于 i − 1 i-1 i−1 的数。
不过有个例外,我们后一个数没法凑,那么没有后一个数不就行了,所以当第二次变数的时候正好是最后一个数的时候就是可以的。也就是像 0 1 1 1 1 1 … 1 n 0\ 1\ 1\ 1\ 1\ 1\dots1\ n 0 1 1 1 1 1…1 n 这种形式。注意因为不能超过 n n n,所以中间只能是 1 1 1,最后为 n n n。
code:
#include <iostream>
#include <cstdio>
using namespace std;
int T,n,m;
int main(){
cin>>T;
while(T--){
cin>>n>>m;
int ans=0;
if(m<=1)ans+=1;
if(m<=2)ans+=n-1;
if(m<=3)ans+=1;
cout<<ans<<endl;
}
return 0;
}
F 随机化游戏时间?
思路1(二分答案):
可以先想想看如何找 l u c k y lucky lucky 数 的最小值,也就是下界。发现可以二分答案,每次二分一个答案,然后记录 ≤ l u c k y \le lucky ≤lucky 数 的前缀和,再看看是否符合所有的要求,即区间 ≤ l u c k y \le lucky ≤lucky 数 的数的个数大于等于每个 k k k 值。
同理我们也可以二分答案算出 l u c k y lucky lucky 数的最大值,也就是上界。每次二分一个答案,然后记录 > l u c k y \gt lucky >lucky 数 的前缀和,再看看是否符合所有的要求,即区间 > l u c k y \gt lucky >lucky 数 的数的个数小于等于每个 L − R + 1 − k L-R+1-k L−R+1−k 值。
code:
#include <iostream>
#include <cstdio>
#include <vector>
using namespace std;
const int maxn=1e5+5;
typedef long long ll;
const ll mod=1e9+7;
ll qpow(ll a,ll b){
ll base=a%mod,ans=1;
b%=mod;
while(b){
if(b&1)ans=ans*base%mod;
base=base*base%mod;
b>>=1;
}
return ans;
}
ll inv(ll x){return qpow(x,mod-2);}
int T,n,m,a[maxn];
int L[maxn],R[maxn],k[maxn];
int pre[maxn];
bool check1(int x){//检查幸运数为x,下界是否合理
for(int i=1;i<=n;i++){
pre[i]=pre[i-1]+(a[i]<=x);
}
for(int i=1;i<=m;i++){
if(pre[R[i]]-pre[L[i]-1]<k[i])
return false;
}
return true;
}
bool check2(int x){//检查幸运数为x,上界是否合理
for(int i=1;i<=n;i++){
pre[i]=pre[i-1]+(a[i]>x);
}
for(int i=1;i<=m;i++){
if(pre[R[i]]-pre[L[i]-1]<R[i]-L[i]+1-k[i])
return false;
}
return true;
}
int main(){
cin>>T;
while(T--){
cin>>n>>m;
for(int i=1;i<=n;i++)
cin>>a[i];
for(int i=1;i<=m;i++)
cin>>L[i]>>R[i]>>k[i];
int ansl,ansr,l,r;
l=1,r=n;
while(l<r){
int mid=l+r>>1;
if(check1(mid))r=mid;
else l=mid+1;
}
ansl=l;
l=1,r=n;
while(l<r){
int mid=l+r+1>>1;
// cout<<l<<" "<<r<<" "<<mid<<endl;
if(check2(mid))l=mid;
else r=mid-1;
}
ansr=r;
if(ansl==ansr)cout<<1<<" "<<ansl<<endl;
else cout<<inv(ansr-ansl+1)<<endl;
}
return 0;
}
思路2(主席树):
我们发现对每个要求,假设区间内第 k k k 大的数为 x 1 x_1 x1,第 k + 1 k+1 k+1 大的数为 x 2 x_2 x2,那么 l u c k y lucky lucky 数 就会处于 [ x 1 , x 2 − 1 ] [x_1,x_2-1] [x1,x2−1] 区间内,找到所有条件的区间,然后取交集就是满足所有条件的 l u c k y lucky lucky 数 的范围,也就是答案。
而静态求区间第 k k k 大,就是主席树板子了。随便找个板子套一下即可。
主席树又叫可持久化线段树,顾名思义就是随着时间变化线段树可以随之维护。我们一般的想法是不同时间下新开一个线段树,但是时间一多,空间和时间的开销比较大。不过我们可以发现线段树上单点修改的话其实只修改了根到一个叶这一条链上所有节点的信息,那么我们不同版本的线段树大部分是一样的,只有一条链不一样,那么我们就可以只新开一个链,而其他节点维持原样,新链连到旧树上即可。
在静态求第 k k k 大时,主席树是多个权值线段树,也就是桶,桶里面存储的是每个数出现的次数,第 i i i 个版本的线段树存储的是 1 ∼ i 1\sim i 1∼i 区间内的所有数的信息。我们要找 [ L , R ] [L,R] [L,R] 区间的数的信息,就用第 R R R 个版本的线段树的每个数的个数减去第 L − 1 L-1 L−1 的线段树,得到的就是中间区间的每个数的信息(也就是出现次数)再在这上面二分找第 k k k 大即可。
这里由于我们找第 k + 1 k+1 k+1 大的数时,最后一个数最多只能找到 n n n,而右端点却取 x − 1 x-1 x−1,所以我主席树多开了一个点,使得最后一个数可以找到 n + 1 n+1 n+1,这样找到最后一个数作为右端点时,减一就变成了 n n n。
code:
#include <iostream>
#include <cstdio>
using namespace std;
typedef long long ll;
const ll mod=1e9+7;
const int maxn=2e5+5;
int T,n,m;
int a[maxn];
ll qpow(ll a,ll b){
ll base=a%mod,ans=1;
b%=mod;
while(b){
if(b&1)ans=ans*base%mod;
base=base*base%mod;
b>>=1;
}
return ans;
}
ll inv(ll x){return qpow(x,mod-2);}
struct HJT_tree{
struct node{
int val;
int ls,rs;
}tr[maxn<<5];
int n;
int rt[maxn],tot;
void push_up(int p){
int ls=tr[p].ls,rs=tr[p].rs;
tr[p].val=tr[ls].val+tr[rs].val;
}
int build(int l,int r){
int p=++tot;
if(l==r){
tr[p].val=0;
tr[p].ls=tr[p].rs=0;
return p;
}
int mid=(l+r)>>1;
tr[p].ls=build(l,mid);
tr[p].rs=build(mid+1,r);
push_up(p);
return p;
}
void build(int _n){
n=_n;
tot=0;
rt[0]=build(1,n);
}
int updata(int l,int r,int q,int x){
int p=++tot;
if(l==r){
tr[p].val=tr[q].val+1;
tr[p].ls=tr[p].rs=0;
return p;
}
int mid=(l+r)>>1;
tr[p].ls=tr[q].ls;
tr[p].rs=tr[q].rs;
if(x<=mid)tr[p].ls=updata(l,mid,tr[q].ls,x);
else tr[p].rs=updata(mid+1,r,tr[q].rs,x);
push_up(p);
return p;
}
int query(int fl,int fr,int l,int r,int k){
int mid=(l+r)>>1,sum=tr[tr[fr].ls].val-tr[tr[fl].ls].val;
// printf("%d %d %d %d %d\n",tr[tr[fr].ls].val,tr[tr[fl].ls].val,l,r,k);
if(l==r){
return l;
}
else if(k<=sum)return query(tr[fl].ls,tr[fr].ls,l,mid,k);
else return query(tr[fl].rs,tr[fr].rs,mid+1,r,k-sum);
}
}tr;
int main(){
cin>>T;
while(T--){
cin>>n>>m;
for(int i=1;i<=n;i++)cin>>a[i];
tr.build(n+1);
for(int i=1;i<=n;i++){
tr.rt[i]=tr.updata(1,n+1,tr.rt[i-1],a[i]);
}
int L=1,R=n;
for(int _=1,l,r,k;_<=m;_++){
cin>>l>>r>>k;
int v=tr.query(tr.rt[l-1],tr.rt[r],1,n+1,k);
int v1=tr.query(tr.rt[l-1],tr.rt[r],1,n+1,k+1);
L=max(L,v);
R=min(R,v1-1);
// cout<<"***"<<v<<" "<<v1<<endl;
}
if(L==R)cout<<1<<" "<<L<<endl;
else cout<<inv(R-L+1)<<endl;
}
return 0;
}
G 随机化游戏时间!
思路(差分约束):
这个题虽然是 F F F 题hard版,但是却和 F F F 题没有任何关系,这题是个差分约束。
关于差分约束我其实了解甚少,网上也没有找到能完全讲明白的博客,所以我也没怎么学会。这里只能在我力所能及范围内尽可能讲讲我的理解,如果有大佬指摘,非常欢迎。
差分约束的简单概念在上面的博客和题目题解里已经讲了很多遍了,我说一下我自己的理解。
一般来说,我们建边是对于 x a − x b ≤ c ⇒ x a ≤ x b + c x_a-x_b\le c\Rightarrow x_a\le x_b+c xa−xb≤c⇒xa≤xb+c 建一条 b → a b\rightarrow a b→a 边权为 c c c 的边,它的含义是 d a ≤ d b + c d_a\le d_b+c da≤db+c。在跑最短路时这条有向边代表着 d a > d b + c d_a>d_b+c da>db+c 则将 d a = d b + c d_a=d_b+c da=db+c 将 a a a 的距离缩减成 d b + c d_b+c db+c。
其实跑完最短路后在图上,对任意一条边,两端的点的距离都一定满足 “边的条件”。也就是 d a ≤ d b + c d_a\le d_b+c da≤db+c。而每个点的距离一定满足最苛刻的那条 d a ≤ d b + c d_a\le d_b+c da≤db+c,准确来说是 d a = d b + c d_a= d_b+c da=db+c,这时 d a d_a da 在满足条件的前提下取得了它能取得的最大值。
反过来,如果对于 x a − x b ≥ c ⇒ x a ≥ x b + c x_a-x_b\ge c\Rightarrow x_a\ge x_b+c xa−xb≥c⇒xa≥xb+c 建一条 b → a b\rightarrow a b→a 边权为 c c c 的边,它的含义是 d a ≥ d b + c d_a\ge d_b+c da≥db+c。在跑最长路时这条有向边代表着 d a < d b + c d_a<d_b+c da<db+c 则将 d a = d b + c d_a=d_b+c da=db+c 将 a a a 的距离增大成 d b + c d_b+c db+c。跑完最长路后每个点的距离一定满足最苛刻的那条 d a ≥ d b + c d_a\ge d_b+c da≥db+c,准确来说是 d a = d b + c d_a= d_b+c da=db+c,这时 d a d_a da 在满足条件的前提下取得了它能取得的最小值。
对于上面给出的两个模板题,一般来说对
x
i
−
x
j
≤
c
x_i-x_j\le c
xi−xj≤c 建立
j
→
i
j\rightarrow i
j→i 边权为
c
c
c 的边
建正向边,边权为正,跑最短路
等价于
建反向边,边权为负,跑最长路
然后再建一个超级源点,向其他所有点连接权值为 0 0 0 的边。这时令源点 d i s = 0 dis=0 dis=0 对于跑最短路相当于其他点都得小于等于 0 0 0,反之对于跑最长路相当于其他点都得大于等于 0 0 0,跑出来的结果就是一正一负。
最后结果数组不太一样,原本是确定最大值为0,其他全为负数。建反向负边现在变成最小值为0,其他全为正数,两个答案之间每个数都差d
code:
建正向边,边权为正,跑最短路。
这时从
0
0
0 开始会得到最大的
l
u
c
k
y
lucky
lucky 数,也就是上界
R
R
R
#include <iostream>
#include <cstdio>
#include <queue>
#include <cstring>
#include <algorithm>
using namespace std;
const int maxn=1e5+5;
int T,n,m;
int head[maxn],ent;
struct edge{
int v,w,nxt;
}e[maxn<<3];
void add(int u,int v,int w){
e[++ent]={v,w,head[u]};
head[u]=ent;
}
queue<int> q;
int d[maxn];
bool vis[maxn];
void spfa(int s){
for(int i=0;i<=n;i++)d[i]=1e9;
q.push(s);
d[s]=0;
vis[s]=true;
while(!q.empty()){
int u=q.front();
q.pop();
vis[u]=false;
for(int i=head[u],v,w;~i;i=e[i].nxt){
v=e[i].v;
w=e[i].w;
if(d[v]>d[u]+w){
d[v]=d[u]+w;
if(!vis[v]){
q.push(v);
vis[v]=true;
}
}
}
}
}
int main(){
cin>>T;
while(T--){
cin>>n>>m;
for(int i=0;i<=n;i++)head[i]=-1;
ent=0;
for(int i=1,l,r,k;i<=m;i++){
cin>>l>>r>>k;
add(l-1,r,k);
add(r,l-1,-k);
}
for(int i=1;i<=n;i++){
add(i-1,i,1);
add(i,i-1,0);
}
int L,R;
spfa(n);
L=-d[0];
spfa(0);
R=d[n];
// cout<<"***"<<L<<" "<<R<<endl;
for(int i=max(1,L);i<=min(n,R);i++){
cout<<i<<" ";
}
cout<<endl;
}
}
建反向边,边权为负,跑最长路
这时从
0
0
0 开始会得到最小的
l
u
c
k
y
lucky
lucky 数,也就是下界
L
L
L
#include <iostream>
#include <cstdio>
#include <queue>
#include <cstring>
#include <algorithm>
using namespace std;
const int maxn=1e5+5;
int T,n,m;
int head[maxn],ent;
struct edge{
int v,w,nxt;
}e[maxn<<3];
void add(int u,int v,int w){
e[++ent]={v,w,head[u]};
head[u]=ent;
}
queue<int> q;
int d[maxn];
bool vis[maxn];
void spfa(int s){
for(int i=0;i<=n;i++)d[i]=-1e9;
q.push(s);
d[s]=0;
vis[s]=true;
while(!q.empty()){
int u=q.front();
q.pop();
vis[u]=false;
for(int i=head[u],v,w;~i;i=e[i].nxt){
v=e[i].v;
w=e[i].w;
if(d[v]<d[u]+w){
d[v]=d[u]+w;
if(!vis[v]){
q.push(v);
vis[v]=true;
}
}
}
}
}
int main(){
cin>>T;
while(T--){
cin>>n>>m;
for(int i=0;i<=n;i++)head[i]=-1;
ent=0;
for(int i=1,l,r,k;i<=m;i++){
cin>>l>>r>>k;
add(l-1,r,k);
add(r,l-1,-k);
}
for(int i=1;i<=n;i++){
add(i,i-1,-1);
add(i-1,i,0);
}
int L,R;
spfa(n);
R=-d[0];
spfa(0);
L=d[n];
// cout<<"***"<<L<<" "<<R<<endl;
for(int i=max(1,L);i<=min(n,R);i++){
cout<<i<<" ";
}
cout<<endl;
}
}