ABC223题解
A
水题,特判 \(0\) 并看 \(X\) 是否能被 \(100\) 整除。
代码:
#include<bits/stdc++.h>
#define dd double
#define ld long double
#define ll long long
#define uint unsigned int
#define ull unsigned long long
#define N 2010
#define M number
using namespace std;
const int INF=0x3f3f3f3f;
template<typename T> inline void read(T &x) {
x=0; int f=1;
char c=getchar();
for(;!isdigit(c);c=getchar()) if(c == '-') f=-f;
for(;isdigit(c);c=getchar()) x=x*10+c-'0';
x*=f;
}
int main(){
int x;read(x);
if(x%100||!x) printf("No");
else printf("Yes");
}
B
注意到字符串长度为 \(1000\),所以直接暴力做出循环串然后排序即可。
也可以用最小表示法,最大的的求解大概和最小差不多,但是比较麻烦。
代码:
#include<bits/stdc++.h>
#define dd double
#define ld long double
#define ll long long
#define uint unsigned int
#define ull unsigned long long
#define N 3020
#define M number
using namespace std;
const int INF=0x3f3f3f3f;
template<typename T> inline void read(T &x) {
x=0; int f=1;
char c=getchar();
for(;!isdigit(c);c=getchar()) if(c == '-') f=-f;
for(;isdigit(c);c=getchar()) x=x*10+c-'0';
x*=f;
}
string s;
string t[N];
int tail,len;
inline string Left(string s){
string now;now.clear();
for(int i=1;i<len;i++) now+=s[i];
now+=s[0];return now;
}
int main(){
cin>>s;t[++tail]=s;
len=s.length();
for(int i=1;i<=len-1;i++){
s=Left(s);t[++tail]=s;
}
sort(t+1,t+tail+1);
cout<<t[1]<<'\n';
cout<<t[tail];
return 0;
}
C
这个题有一点诈骗,一开始想两边同时走,但这样太难于实现,不难发现每个人走的时间相同,所以我们只要处理处每个人走的时间然后直接走就可以。
代码:
#include<bits/stdc++.h>
#define dd double
#define ld long double
#define ll long long
#define uint unsigned int
#define ull unsigned long long
#define N 100010
#define M number
using namespace std;
const int INF=0x3f3f3f3f;
template<typename T> inline void read(T &x) {
x=0; int f=1;
char c=getchar();
for(;!isdigit(c);c=getchar()) if(c == '-') f=-f;
for(;isdigit(c);c=getchar()) x=x*10+c-'0';
x*=f;
}
int n;
struct Node{
dd a,b;
}c[N];
dd t,dis;
int main(){
read(n);
for(int i=1;i<=n;i++){
read(c[i].a);read(c[i].b);
t+=c[i].a/c[i].b;
}
t/=2;
for(int i=1;i<=n;i++){
if(t*c[i].b>=c[i].a){
t-=c[i].a/c[i].b;
dis+=c[i].a;
}
else{
dis+=c[i].b*t;
break;
}
}
printf("%0.10lf\n",dis);
return 0;
}
D
就是求拓扑排序,至于字典序最小,我们可以用优先队列来做。
代码:
#include<bits/stdc++.h>
#define dd double
#define ld long double
#define ll long long
#define uint unsigned int
#define ull unsigned long long
#define N 200010
#define M number
using namespace std;
const int INF=0x3f3f3f3f;
template<typename T> inline void read(T &x) {
x=0; int f=1;
char c=getchar();
for(;!isdigit(c);c=getchar()) if(c == '-') f=-f;
for(;isdigit(c);c=getchar()) x=x*10+c-'0';
x*=f;
}
struct edge{
int to,next;
inline void Init(int to_,int ne_){
to=to_;next=ne_;
}
}li[N];
int head[N],tail;
inline void Add(int from,int to){
li[++tail].Init(to,head[from]);
head[from]=tail;
}
int n,m,rd[N],xulie[N],tt;
struct cmp{
inline bool operator () (int a,int b){return a>b;}
};
priority_queue<int,vector<int>,cmp> q;
inline void bfs(){
while(q.size()){
int top=q.top();q.pop();
xulie[++tt]=top;
for(int x=head[top];x;x=li[x].next){
int to=li[x].to;rd[to]--;
if(!rd[to]) q.push(to);
}
}
}
int main(){
read(n);read(m);
for(int i=1;i<=m;i++){
int from,to;read(from);read(to);
Add(from,to);rd[to]++;
}
for(int i=1;i<=n;i++) if(!rd[i]) q.push(i);
bfs();
if(tt!=n) return puts("-1"),0;
for(int i=1;i<=n;i++) printf("%d ",xulie[i]);
return 0;
}
E
这个题比赛场上没有做出来,实际上这个题还是有一点技巧的,这个题启示我从简单情况开始考虑,然后一步步想出正确的贪心策略。
首先我们考虑放两个矩形的情形,不难发现,如果有合法方案的话,一定存在一条线,然后这两个矩形分别在线的两边,并且这个线满足平行于 \(x\) 轴或 \(y\) 轴。
上面这个性质不难证明,我们考虑如何放置。
我们考虑第一个矩形如何放,我们先假设这条线平行于 \(x\) 轴,设这个矩形的大小为 \(S\),那么我们就令 \(x=\left\lceil \frac{S}{X} \right\rceil\),其中 \(X\) 为题目给定的放置的地方大小。
然后我们在上面放第二个矩形,只需要检查其面积即可。
然后我们考虑三个矩形的情形,有一个性质是我们仍然可以画一条平行于 \(x\) 轴或平行于 \(y\) 轴的直线,满足一边有一个矩形,另一边有两个矩形。
以上性质通过反证法不难证明。
所以我们仍然可以先看这条线,贪心的放一个矩形,然后问题转化成了上面那个问题。
不得不说 std 实现的是真的好,放哪个矩形,平行于哪个轴,都可以通过交换来实现,最终本题可以 \(O(1)\) 解决。
代码:
#include<bits/stdc++.h>
#define dd double
#define ld long double
#define int long long
#define ll long long
#define uint unsigned int
#define ull unsigned long long
#define N number
#define M number
using namespace std;
const int INF=0x3f3f3f3f;
template<typename T> inline void read(T &x) {
x=0; int f=1;
char c=getchar();
for(;!isdigit(c);c=getchar()) if(c == '-') f=-f;
for(;isdigit(c);c=getchar()) x=x*10+c-'0';
x*=f;
}
int x,y,a,b,c;
inline bool Check(int x,int y,int b,int c){
for(int i=1;i<=2;i++){
int len=(b+x-1)/x;
if(len<y&&(y-len)*x>=c) return 1;
swap(x,y);
}
return 0;
}
inline bool Solve(int x,int y,int a,int b,int c){
for(int i=1;i<=2;i++){
for(int j=1;j<=3;j++){
int len=(a+x-1)/x;
if(len<y&&Check(x,y-len,b,c)) return 1;
swap(a,b);swap(b,c);
}
swap(x,y);
}
return 0;
}
signed main(){
read(x);read(y);read(a);read(b);read(c);
puts(Solve(x,y,a,b,c)?"Yes":"No");
return 0;
}
F
这个题在思维难度上比 E 题要简单许多,一看就是一个线段树小清新题。
题目是维护某个区间的括号序列是否合法,带修改。
考虑到如果一个区间内的括号序列不合法,那么让合法的全部消掉之后一定变成了这样:
我们考虑维护能消的都消去之后左括号和右括号的数量,然后合并的时候注意消去就可以。
如果一段区间内的括号序列合法,那么合并的结果括号数量一定为 \(0\)。
代码:
#include<bits/stdc++.h>
#define dd double
#define ld long double
#define ll long long
#define uint unsigned int
#define ull unsigned long long
#define N 2000100
#define M number
using namespace std;
const int INF=0x3f3f3f3f;
template<typename T> inline void read(T &x) {
x=0; int f=1;
char c=getchar();
for(;!isdigit(c);c=getchar()) if(c == '-') f=-f;
for(;isdigit(c);c=getchar()) x=x*10+c-'0';
x*=f;
}
int n,m;
char s[N];
struct Node{
int lt,rt;
inline Node(){lt=0;rt=0;}
inline Node operator + (const Node &b)const{
Node a;a.lt=a.rt=0;
a.rt=rt;a.lt=b.lt;
if(lt<b.rt) a.rt+=b.rt-lt;
else a.lt+=lt-b.rt;
return a;
}
}p[N<<2];
#define ls k<<1
#define rs k<<1|1
struct SegmentTree{
inline void PushUp(int k){
// if(p[ls].lt==p[rs].rt){p[k].rt=p[ls].rt;p[k].lt=p[rs].lt;}
// else if(p[ls].lt<p[rs].rt){p[k].rt=p[ls].rt+p[rs].rt-p[ls].lt;p[k].lt=p[rs].lt;}
// else{p[k].rt=p[ls].rt;p[k].lt=p[ls].lt-p[rs].rt+p[rs].lt;}
p[k]=p[ls]+p[rs];
}
inline void Build(int k,int l,int r){
if(l==r){
if(s[l]=='(') p[k].lt++;
else p[k].rt++;return;
}
int mid=(l+r)>>1;
Build(ls,l,mid);Build(rs,mid+1,r);
PushUp(k);
}
inline void Change(int k,int l,int r,int w){
if(l==r){
p[k].lt^=1;p[k].rt^=1;return;
}
int mid=(l+r)>>1;
if(w<=mid) Change(ls,l,mid,w);
else Change(rs,mid+1,r,w);
PushUp(k);
}
inline Node Ask(int k,int l,int r,int z,int y){
if(l==z&&r==y){return p[k];}
int mid=(l+r)>>1;
if(y<=mid) return Ask(ls,l,mid,z,y);
else if(z>mid) return Ask(rs,mid+1,r,z,y);
else return Ask(ls,l,mid,z,mid)+Ask(rs,mid+1,r,mid+1,y);
}
}st;
int main(){
// freopen("my.in","r",stdin);
// freopen("my.out","w",stdout);
read(n);read(m);
scanf("%s",s+1);
st.Build(1,1,n);
for(int i=1;i<=m;i++){
int op,l,r;read(op);read(l);read(r);
if(op==1){
if(s[l]==s[r]) continue;
swap(s[l],s[r]);
st.Change(1,1,n,l);st.Change(1,1,n,r);
}
else{
Node ans=st.Ask(1,1,n,l,r);
if(ans.lt==0&&ans.rt==0){
puts("Yes");
}
else puts("No");
}
}
return 0;
}
G
G 题是一个比较复杂的换根 dp,换根 dp 的题我也不常做,这个题算是帮我熟悉一下。
因为要对每个节点求,所以不难想到是一个换根 dp,然后我们考虑先求出整棵树初始的答案。
设 \(f_{k,0/1}\) 表示这个节点在与其儿子匹配或不匹配的条件下,\(k\) 这颗子树的答案是多少,思考之后不难得出转移:
注意到我们可以先做第一个转移,然后第二个转移枚举一遍就可以了,具体转移细节看代码。这个 dp 的复杂度为 \(O(n)\)。
然后我们考虑如何计算去掉每一个节点及其连边后的匹配。首先我们默认以 \(1\) 为根。
注意到如果我们把 \(k\) 去掉,这个答案由这样几部分组成:
-
\(k\) 的儿子的子树,这个答案就是 \(\sum_{s\in son_k}\max(f_{s,0},f_{s,1})\)
-
除去 \(k\) 的子树的部分,这个部分我们设为 \(g_k\)。
然后我们考虑如何处理出 \(g_{k}\),首先我们还是要考虑 \(k\) 与其父亲的连边,设 \(g_{k,0/1}\) 表示考虑整棵树去掉 \(k\) 的子树的剩下部分的贡献,\(0/1\) 表示 \(k\) 和其父亲的连边选还是不选。设 \(k\) 的父亲为 \(fa\)。
考虑转移,不难得到:
解释一下 \(g_{k,0}\) 的转移,第一个式子考虑的是 \(fa\) 与其中一个儿子相连,第二个式子考虑的是 \(fa\) 与其父亲相连或没有相连。
第一个转移不难做,第二个转移的第二个式子不难做,我们考虑如果做第二个转移的第一个式子。
不难发现一个式子,对于大部分的 \(k\),\(u\) 的选择都是一定的。这个 \(u\) 可以贪心的求出,对于 \(k\neq u\),我们直接转移就可以,这个不难做,对与 \(k=u\) 我们再去枚举。
整个 dp 的复杂度仍然可以做到线性。
然后处理出去掉每个节点的答案,统计即可。
代码:
#include<bits/stdc++.h>
#define dd double
#define ld long double
#define ll long long
#define uint unsigned int
#define ull unsigned long long
#define N 200010
#define M number
using namespace std;
const int INF=0x3f3f3f3f;
template<typename T> inline void read(T &x) {
x=0; int f=1;
char c=getchar();
for(;!isdigit(c);c=getchar()) if(c == '-') f=-f;
for(;isdigit(c);c=getchar()) x=x*10+c-'0';
x*=f;
}
template<typename T> inline T Max(T a,T b){return a<b?b:a;}
struct edge{
int to,next;
inline void Init(int to_,int ne_){
to=to_;next=ne_;
}
}li[N<<1];
int head[N],tail;
inline void Add(int from,int to){
li[++tail].Init(to,head[from]);
head[from]=tail;
}
int f[N][2],g[N][2],ans[N],Ans,n;
inline void Dfs(int k,int fa){
bool op=0;
for(int x=head[k];x;x=li[x].next){
int to=li[x].to;
if(to==fa) continue;
Dfs(to,k);f[k][0]+=Max(f[to][1],f[to][0]);
}
for(int x=head[k];x;x=li[x].next){
int to=li[x].to;
if(to==fa) continue;
f[k][1]=Max(f[k][1],f[k][0]-Max(f[to][1],f[to][0])+f[to][0]+1);
op=1;
}
if(!op) f[k][1]=-INF;
// printf("f[%d][0]=%d\nf[%d][1]=%d\n",k,f[k][0],k,f[k][1]);
}
inline void Init(){
read(n);
for(int i=1;i<=n-1;i++){
int from,to;read(from);read(to);
Add(from,to);Add(to,from);
}
}
inline void Dp(int k,int fa){
int sum=0,X=-1;
for(int x=head[k];x;x=li[x].next){
int to=li[x].to;
if(to==fa) continue;
sum+=Max(f[to][0],f[to][1]);
if(f[to][0]>=f[to][1]) X=to;
}
if(X==-1){
int cha=INF;
for(int x=head[k];x;x=li[x].next){
int to=li[x].to;
if(to==fa) continue;
if(f[to][1]-f[to][0]<cha){
cha=f[to][1]-f[to][0];
X=to;
}
}
}
for(int x=head[k];x;x=li[x].next){
int to=li[x].to;
if(to==fa) continue;
g[to][1]=sum-Max(f[to][0],f[to][1])+g[k][0]+1;
g[to][0]=sum-Max(f[to][0],f[to][1])+Max(g[k][0],g[k][1]);
}
for(int x=head[k];x;x=li[x].next){
int to=li[x].to;
if(to==fa||to==X) continue;
g[to][0]=Max(g[to][0],sum-Max(f[to][0],f[to][1])-Max(f[X][0],f[X][1])+f[X][0]+1+g[k][0]);
g[X][0]=Max(g[X][0],sum-Max(f[X][0],f[X][1])-Max(f[to][0],f[to][1])+f[to][0]+1+g[k][0]);
}
for(int x=head[k];x;x=li[x].next){
int to=li[x].to;
if(to==fa) continue;
Dp(to,k);
}
}
inline void Calc(int k,int fa){
ans[k]=g[k][0];
// printf("g[%d][0]=%d\ng[%d][1]=%d\n",k,g[k][0],k,g[k][1]);
for(int x=head[k];x;x=li[x].next){
int to=li[x].to;
if(to==fa) continue;
ans[k]+=Max(f[to][0],f[to][1]);
Calc(to,k);
}
}
inline void Print(){
int tot=0;
Ans=Max(f[1][0],f[1][1]);
// printf("Ans=%d\n",Ans);
for(int i=1;i<=n;i++){
if(Ans==ans[i]) tot++;
// printf("ans[%d]=%d\n",i,ans[i]);
}
printf("%d\n",tot);
}
int main(){
// freopen("my.in","r",stdin);
// freopen("my.out","w",stdout);
Init();Dfs(1,0);Dp(1,0);Calc(1,0);Print();
}
H
第一次做线性基的题目。
我们考虑到集合异或,一定是线性基,不难判断一个数是否通过某个集合异或出来,我们考虑如何做那个区间限制。
首先可以线段树线性基暴力合并,然后查找区间,这样复杂度大概可以过?(不知道,没写过)
我们考虑转化题目条件,\(x\) 可以被 \([l,r]\) 中的数异或出来相当于我们在 \([1,r]\) 这个前缀中异或,并且用到的数都是大于等于 \(l\) 的,如果要维护这个信息,我们必须要考虑如果让我们构造出来的线性基编号尽可能的大。
构造过程具体看代码,代码后我们来证明一下构造的正确性。
构造出来后,直接查询即可。
代码:
#include<bits/stdc++.h>
#define dd double
#define ld long double
#define ll long long
#define int long long
#define uint unsigned int
#define ull unsigned long long
#define N 66
#define M 400010
using namespace std;
const int INF=0x3f3f3f3f;
template<typename T> inline void read(T &x) {
x=0; int f=1;
char c=getchar();
for(;!isdigit(c);c=getchar()) if(c == '-') f=-f;
for(;isdigit(c);c=getchar()) x=x*10+c-'0';
x*=f;
}
struct LinearBasis{
int b[N],ID[N];
inline void Insert(int val,int id){
for(int i=60;i>=0;i--){
if(((val>>i)&1)==0) continue;
if(!b[i]){b[i]=val;ID[i]=id;return;}
else{
if(ID[i]<id){swap(ID[i],id);swap(val,b[i]);}
val^=b[i];
}
}
}
inline bool Ask(int val,int l){
for(int i=60;i>=0;i--){
if(((val>>i)&1)==0) continue;
if(!b[i]||ID[i]<l) return 0;
val^=b[i];
}
return 1;
}
}LB[M];
int n,q;
signed main(){
// freopen("my.in","r",stdin);
// freopen("my.out","w",stdout);
read(n);read(q);
for(int i=1;i<=n;i++){
int x;read(x);LB[i]=LB[i-1];LB[i].Insert(x,i);
}
for(int i=1;i<=q;i++){
int l,r,x;read(l);read(r);read(x);
puts(LB[r].Ask(x,l)?"Yes":"No");
}
return 0;
}
考虑证明正确性:
-
首先,这个线性基仍然是线性无关的,这个非常显然。
-
然后我们考虑为什么交换之后这个线性基依旧是正确的,我们考虑是 \(u\) 把 \(v\) 换掉,随后 \(v\) 变成 \(v\ xor\ u\)
我们发现这和朴素的线性基构造只有一个区别,就是这个位置上的数变了,往下走的数并没有变化。
我们考虑这个东西是否满足线性基性质,不难发现,我们不用考虑某些位置因为之前插入的时候异或的是 \(v\) 而无法构造,事实上,我们往下插的时候,相当于插入了 \(v\),所以这个线性基一定是可以异或出 \(v\) 的,遍历到的位置变成了那个数异或 \(u\),没有被遍历到的不参与构成 \(v\)。