ABC337 题解
前言:
提交洛谷题解的部分会详细一些。
A
题意
比较两人总得分,判断胜者或平局。
Sol
字面意思模拟即可。
Code
#include<bits/stdc++.h>
#define ll long long
#define N 200005
#define endl "\n"
#define fi first
#define se second
using namespace std;
const ll mod=1e9+7;
const ll inf=1e18;
const double eps=1e-6;
ll n,a[N],m,k;
ll f[N],res,sum,ans;
string s;
int main(){
//freopen(".in","r",stdin);
//freopen(".out","w",stdout);
cin>>n;
ll x=0,y=0;
for(int i=1;i<=n;i++){
cin>>x>>y;
res+=x;
res-=y;
}
if(res>0)cout<<"Takahashi";
if(res==0)cout<<"Draw";
if(res<0)cout<<"Aoki";
return 0;
}
B
题意
判断字符串是否含有逆序对( \(B\) 后面有 \(A\) 或 \(C\) 后面有\(A\) 或 \(B\) )。
Sol
字面意思模拟即可。
Code
#include<bits/stdc++.h>
#define ll long long
#define N 200005
#define endl "\n"
#define fi first
#define se second
using namespace std;
const ll mod=1e9+7;
const ll inf=1e18;
const double eps=1e-6;
ll n,a[N],m,k;
ll f[N],res,sum,ans;
string s;
int main(){
//freopen(".in","r",stdin);
//freopen(".out","w",stdout);
cin>>s;
n=s.size();
s=" "+s;
ll a=0,b=0,c=0;
for(int i=1;i<=n;i++){
if(s[i]=='A'){
if(b||c){
cout<<"No\n";
return 0;
}
a=1;
}
if(s[i]=='B'){
if(c){
cout<<"No\n";
return 0;
}
b=1;
}
if(s[i]=='C'){
c=1;
}
}
cout<<"Yes\n";
return 0;
}
C
题意
给定 \(n\) 个人每个人前面是谁,还原排队的顺序。
Sol
记录每个人后面跟着谁,从对头开始递归找即可。
Code
#include<bits/stdc++.h>
#define ll long long
#define N 400005
#define endl "\n"
#define fi first
#define se second
using namespace std;
const ll mod=1e9+7;
const ll inf=1e18;
const double eps=1e-6;
ll n,a[N];
string s;
int main(){
//freopen(".in","r",stdin);
//freopen(".out","w",stdout);
cin>>n;
ll l=0;
for(int i=1;i<=n;i++){
ll x;
cin>>x;
if(x==-1)l=i;
else{
a[x]=i;
}
}
while(a[l]){
cout<<l<<" ";
l=a[l];
}
cout<<l<<" ";
return 0;
}
D
题意
一个 \(n\times m\) 矩阵,选取一个 \(k\times1\) 或 \(1\times k\) 的区域,这个区域不能含有 x
,找到一个含有最少 .
的区域并输出这个最小值,或报告无解。
Sol
把 .
值设为 \(1\),把 o
值设为 \(0\),把 x
值设为 inf
,把列和行单独分开来做,动态查前缀和,即求 \(sum_i-sum_{i-k}\),如果这东西绝对值比 \(k\) 小,说明是一种合法答案。
但题目没有规定 \(n\),\(m\) 的单独范围,直接开二维存图会炸。一种解决方式是用 vector
。这里介绍一种另类的方式。
行的情况很好维护,读入时顺带处理即可,当然也可以用下面这种。
重点在列,先考虑正常前缀和,\(w_{i,j}\) 维护前 \(i\) 行,第 \(j\) 列的前缀和,然后求的是 \(w_{i,j}-w_{i-k,j}\),注意到每次查询区间长度固定,很像滑动窗口,只需要维护前 \(k\) 行位置的答案即可,那么就可以去掉一维,\(w_j\) 表示第 \(j\) 列 \(i-k+1 \sim i\) 位置的和,用一个队列存每行的字符串,动态删除超过位置的前缀即可。
具体实现见代码,复杂度 \(O(nm)\)。
Code
#include<bits/stdc++.h>
#define ll long long
#define N 400005
#define endl "\n"
#define fi first
#define se second
using namespace std;
const ll mod=1e9+7;
const ll inf=1e9;
const double eps=1e-6;
ll n,m,k;
ll sum[N],w[N];
queue<string>q;//好似string内部是vector所以不会炸
string s;
int main(){
//freopen(".in","r",stdin);
//freopen(".out","w",stdout);
cin>>n>>m>>k;
ll res=inf;
for(int i=1;i<=n;i++){
cin>>s;
s=" "+s;
q.push(s);
for(int j=1;j<=m;j++){
sum[j]=sum[j-1];//这个是行的
if(s[j]=='x')sum[j]+=inf,w[j]+=inf;
if(s[j]=='.')sum[j]++,w[j]++;//行加,列也要加
if(abs(sum[j]-sum[j-k])<=k&&j>=k)res=min(res,sum[j]-sum[j-k]);//维护行的答案
}
if(i>=k){
s=q.front();
q.pop();
for(int j=1;j<=m;j++){
if(abs(w[j])<=inf)res=min(res,w[j]);//维护列的答案
if(s[j]=='x')w[j]-=inf;//新增一列就得删除一列
if(s[j]=='.')w[j]--;
}
}
}
if(res==inf)res=-1;
cout<<res;
return 0;
}
E
题意
交互题,\(n\) 瓶果汁其中一瓶有毒,可以给任意个人喝任意数量的果汁,确定有毒的那一瓶,并最小化喝果汁的人数。
第一次输入,给定 \(n\)。
输出最小喝药水人数 \(m\)。
以及对应每个人喝了多少瓶果汁,以及喝了哪些果汁。
第二次输入,根据你分配喝果汁的情况,返回谁会中毒。
输出中毒的是哪一瓶。
Sol
每个人每瓶果汁只有喝和不喝两种状态,这本质上就是二进制。用二进制表示每瓶果汁的饮用情况,这样就只需要 \(\lceil \log n \rceil\) 名朋友就能完成试毒。
更具体的说,第 \(i\) 瓶果汁的饮用情况拆成 \(i-1\) 对应的二进制表示,(\(0\) 也能用到,不能浪费),第 \(j\) 位为 \(1\) 表示第 \(j\) 个人喝了这瓶果汁。
手搓几组数据试一试,结果如下。
1:
0
2:
01
3:
010
001
4:
0101
0011
5:
01010
00110
00001
6:
010101
001100
000011
7
0101010
0011001
0000111
8
01010101
00110011
00001111
结果返回的是那些人中毒了,把这些中毒的人所在位置代表的二进制相加,得到有毒果汁的唯一s编号。
Code
#include<bits/stdc++.h>
#define ll int
#define N 205
#define endl "\n"
#define fi first
#define se second
using namespace std;
const ll mod=1e9+7;
const ll inf=1e18;
const double eps=1e-6;
ll n,lg[N];
string s;
ll f[N][N],sum[N];
int main(){
//freopen(".in","r",stdin);
//freopen(".out","w",stdout);
cin>>n;
ll t=__lg(n-1)+1;
cout<<t<<endl;
ll fl=0,q=0,now=1;;
for(ll i=1;i<=t;i++){
fl=0,q=0;
for(ll j=1;j<=n;j++){
f[i][j]=fl;
q++;
if(q==now){
q=0;
fl^=1;
}
sum[i]+=f[i][j];
}
now*=2;
}
for(ll i=1;i<=t;i++){
cout<<sum[i]<<" ";
for(ll j=1;j<=n;j++){
if(f[i][j])cout<<j<<" ";
}
cout<<endl;
}
fflush(stdout);
cin>>s;
ll res=0;
for(ll i=0;i<t;i++)if(s[i]=='1')res+=(1<<i);
cout<<res+1<<endl;
fflush(stdout);
return 0;
}
//
//01001011
//00101101
//00010111
F
题意
\(n\) 个物品构成一个环,从第 \(p\) 个物品开始顺时针拿,拿到 \(p-1\) 个物品。每次将一个物品放到盒子当中,总共 \(m\) 个盒子,一个盒子中最多放 \(k\) 个物品。优先将同类型的物品放到一个盒子当中,如果放满了则向后新找一个空盒子,如果没有空盒子就扔掉这个物品,注意,一个盒子当中只能放一种类型的物品。求盒子中的球的数量。
求所有 \(p\) 的答案。
Sol
先断环为链,复制一份变成 \(2n\) 个物品。然后动态维护固定区间长度,左右端点答案。给每个类型开个桶 \(f_{a_i}\) 存区间数量。
右端点向右新增物品,加上当前物品的影响。如果 \(f_{a_i} \equiv 1 \ \left(\mod k \ \right)\),说明需要新开一个盒子,如果盒子数量比 \(m\) 小,那么答案增加 \(\min (k,sum_{a_i}-f_{a_i}+1)\),很好理解,如果剩下的物品数比 \(k\) 多,这个盒子只能放 \(k\) 个,比 \(k\) 少,只能放总个数减去已经放进去的个数。如果盒子数量大于等于 \(m\),就不能再放新的物品,那就不管了直接扔掉。
左端点向右删去物品,删去上个物品的影响。减去数量,然后如果 \(f_{a_i} \equiv 0 \ \left(\mod k \ \right)\) 盒子数量减一,答案减去删去盒子的物品数量。
Code
#include<bits/stdc++.h>
#define ll long long
#define N 500005
#define endl "\n"
#define fi first
#define se second
using namespace std;
const ll mod=1e9+7;
const ll inf=1e18;
const double eps=1e-6;
ll n,m,k;
ll a[N],f[N],sum[N];
int main(){
//freopen(".in","r",stdin);
//freopen(".out","w",stdout);
cin>>n>>m>>k;
for(int i=1;i<=n;i++){
cin>>a[i];
sum[a[i]]++;
a[i+n]=a[i];
}
ll cnt=0,j=1,ans=0;
for(int i=1;i<=n;i++){
while(cnt<m&&j<i+n){
f[a[j]]++;
if((f[a[j]]-1)%k==0){
cnt++;
ans+=min(k,sum[a[j]]-f[a[j]]+1);
}
j++;
}
cout<<ans<<endl;
f[a[i]]--;
if((f[a[i]])%k==0){
cnt--;
ans-=min(k,sum[a[i]]-f[a[i]]);
}
}
return 0;
}
G
题意
一颗 \(n\) 个节点的树,求所有 \(u \in [1,n]\),以 \(u\) 为根时,求点对 \((v,w)\) 数,满足 \(v< w\) 且 \(w\) 在 \(u \rightarrow v\) 的路径上。
Sol
就如题目所说的,采用 换根 \(dp\)。这里先规定一些名称。
记 \(f_i\) 表示以 \(i\) 为根的子树的答案。
记 \(a_i\) 表示以 \(i\) 为根的子树内小于 \(\large {i}\) 的节点数量。
记 \(h_i\) 表示以 \(i\) 为根的子树内小于 \(\large farher_{i}\) 的节点数量。
记 \(g_i\) 表示以 \(i\) 为根的树的答案。
根据换根 \(dp\) 惯例,先考虑钦定 \(1\)为根节点,求出 \(f_{1}\)。
由题目描述,不难找出关系式。
也就是:
而 \(g_1=f_1\)。
接着考虑换根。
原来以 \(x\) 为根,现在要把 \(y\) 换成根,维护换根后 \(g_y\) 基于 \(g_x\) 的变化。
红色部分原来是 \(x\) 的子树,换根后要减去这部分对 \(x\) 的贡献,也就是减去 \(y\) 子树内小于 \(x\) 的结点数,即减去 \(h_y\)。
蓝色部分原来不是 \(y\) 的子树,换根后要加上这部分对 \(y\) 的贡献,也就是加上 \(y\) 子树外小于 \(y\) 的节点数,小于 \(y\) 的节点树只有 \(y-1\) 个,子树内占了 \(a_y\) 个,所以要加上 \(y-1-a_y\) 。
所以:
到这里推到出所有公式都比较简单,那么如何得到 \(a_i\),\(h_i\) 呢?
类似前缀和的思想,记录递归 \(x\) 子树之前的答案,以及递归 \(x\) 子树之后的答案,后者减去前者即可,用树状数组或线段树都可以做。
当然也可以可持久化。
时间复杂度 \(O(nlogn)\)。
Code
#include<bits/stdc++.h>
#define ll long long
#define N 500005
#define endl "\n"
#define fi first
#define se second
using namespace std;
const ll mod=1e9+7;
const ll inf=1e18;
const double eps=1e-6;
ll n;
ll f[N],g[N],a[N],h[N],tr[N];
//ai:以i为根的子树内比i小的数的个数
//fi:以i为根的子树的答案
//fu+=fv+au
//gv=gu-fv
vector<ll>v[N];
void add(ll x,ll t){
for(int i=x;i<=n;i+=(i&(-i)))tr[i]+=t;
}
ll qr(ll x){
ll ans=0;
for(int i=x;i>0;i-=(i&(-i)))ans+=tr[i];
return ans;
}
void dfs1(ll x,ll fa){
a[x]=-qr(x-1);
h[x]=-qr(fa-1);
//要在查完fa后再加入x,否则会导致x<fa的情况无法被统计到
for(auto y:v[x])if(y!=fa)dfs1(y,x);
add(x,1);
a[x]+=qr(x-1);
h[x]+=qr(fa-1);
f[x]=a[x];
for(auto y:v[x])if(y!=fa)f[x]+=f[y];
}
void dfs2(ll x,ll fa){
for(auto y:v[x]){
if(y==fa)continue;
g[y]=g[x]-h[y]+(y-a[y]-1);
dfs2(y,x);
}
}
int main(){
//freopen(".in","r",stdin);
//freopen(".out","w",stdout);
cin>>n;
for(int i=1,x,y;i<n;i++){
cin>>x>>y;
v[x].push_back(y);
v[y].push_back(x);
}
dfs1(1,0);
g[1]=f[1];
dfs2(1,0);
for(int i=1;i<=n;i++)cout<<g[i]<<" ";
return 0;
}