前言
感觉这场的题全是 guess 和 attention,做起来难的一批......
一、A. Disturbing Distribution
cpp
#include <bits/stdc++.h>
using namespace std;
/* /\_/\
* (= ._.)
* / > \>
*/
/*
*想好再写
*注意审题 注意特判
*不要红温 不要急躁 耐心一点
*WA了不要立马觉得是思路不对 先耐心找反例
*/
#define endl '\n'
#define dbg(x) cout<<#x<<endl;cout<<x<<endl;
#define vdbg(a) cout<<#a<<endl;for(auto x:a)cout<<x<<" ";cout<<endl;
#define YES cout<<"YES"<<endl;return ;
#define Yes cout<<"Yes"<<endl;return ;
#define NO cout<<"NO"<<endl;return ;
#define No cout<<"No"<<endl;return ;
#define popcount __builtin_popcount
using ll=long long;
using ld=long double;
using pii=pair<int,int>;
using pll=pair<ll,ll>;
const int INF=1e9;
const ll INFLL=1e18;
const int dx[]={-1,1,0,0};
const int dy[]={0,0,-1,1};
const int ddx[]={-2,-1,1,2,2,1,-1,-2};
const int ddy[]={1,2,2,1,-1,-2,-2,-1};
void solve()
{
int n;
cin>>n;
vector<int>a(n+1);
for(int i=1;i<=n;i++)
{
cin>>a[i];
}
int ans=0;
int one=0;
for(int i=1;i<=n;i++)
{
if(a[i]==1)
{
one=1;
}
else
{
ans+=a[i];
one=0;
}
}
cout<<ans+one<<endl;
}
void init()
{
}
signed main()
{
ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
int t=1;
cin>>t;
init();
while(t--)
{
solve();
}
return 0;
}
对于正整数,越乘代价肯定越大,所以肯定是每次只选一个数最优。而对于 1,其可以和大于 1 的数组合消耗掉,最后对于剩下的若干个 1 一起选只计算一次代价即可。那么就是顺序遍历,每找到一个大于 1 的数就让其和前面的 1 配对,维护一下上次配对之后有无 1 即可。
二、B. Everything Everywhere
cpp
#include <bits/stdc++.h>
using namespace std;
/* /\_/\
* (= ._.)
* / > \>
*/
/*
*想好再写
*注意审题 注意特判
*不要红温 不要急躁 耐心一点
*WA了不要立马觉得是思路不对 先耐心找反例
*/
#define endl '\n'
#define dbg(x) cout<<#x<<endl;cout<<x<<endl;
#define vdbg(a) cout<<#a<<endl;for(auto x:a)cout<<x<<" ";cout<<endl;
#define YES cout<<"YES"<<endl;return ;
#define Yes cout<<"Yes"<<endl;return ;
#define NO cout<<"NO"<<endl;return ;
#define No cout<<"No"<<endl;return ;
#define popcount __builtin_popcount
using ll=long long;
using ld=long double;
using pii=pair<int,int>;
using pll=pair<ll,ll>;
const int INF=1e9;
const ll INFLL=1e18;
const int dx[]={-1,1,0,0};
const int dy[]={0,0,-1,1};
const int ddx[]={-2,-1,1,2,2,1,-1,-2};
const int ddy[]={1,2,2,1,-1,-2,-2,-1};
template<typename T>
T gcd(T a,T b){
return b==0?a:gcd(b,a%b);
}
template<typename T>
T lcm(T a,T b){
return a/gcd(a,b)*b;
}
void solve()
{
int n;
cin>>n;
vector<int>a(n+1);
for(int i=1;i<=n;i++)
{
cin>>a[i];
}
ll ans=0;
for(int i=2;i<=n;i++)
{
if(gcd(a[i],a[i-1])==max(a[i],a[i-1])-min(a[i],a[i-1]))
{
ans++;
}
}
cout<<ans<<endl;
}
void init()
{
}
signed main()
{
ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
int t=1;
cin>>t;
init();
while(t--)
{
solve();
}
return 0;
}
令区间 gcd 为 g,那么区间 min/max 肯定都是 g 的倍数,那么就有 。那么也就有
,所以就要求最大最小值之间不能有任何其他 g 的倍数出现。又因为是排列,所以合法的区间长度必定为 2。那么就只需要遍历一遍,每次 check 相邻两个位置即可。
三、C. Mental Monumental (Easy Version)
cpp
#include <bits/stdc++.h>
using namespace std;
/* /\_/\
* (= ._.)
* / > \>
*/
/*
*想好再写
*注意审题 注意特判
*不要红温 不要急躁 耐心一点
*WA了不要立马觉得是思路不对 先耐心找反例
*/
#define endl '\n'
#define dbg(x) cout<<#x<<endl;cout<<x<<endl;
#define vdbg(a) cout<<#a<<endl;for(auto x:a)cout<<x<<" ";cout<<endl;
#define YES cout<<"YES"<<endl;return ;
#define Yes cout<<"Yes"<<endl;return ;
#define NO cout<<"NO"<<endl;return ;
#define No cout<<"No"<<endl;return ;
#define popcount __builtin_popcount
using ll=long long;
using ld=long double;
using pii=pair<int,int>;
using pll=pair<ll,ll>;
const int INF=1e9;
const ll INFLL=1e18;
const int dx[]={-1,1,0,0};
const int dy[]={0,0,-1,1};
const int ddx[]={-2,-1,1,2,2,1,-1,-2};
const int ddy[]={1,2,2,1,-1,-2,-2,-1};
void solve()
{
int n;
cin>>n;
vector<int>a(n+1);
int mx=-1;
for(int i=1;i<=n;i++)
{
cin>>a[i];
mx=max(mx,a[i]);
}
auto check=[&](int mid)->bool
{
multiset<int>st;
vector<int>vis(mid);
for(int i=1;i<=n;i++)
{
if(a[i]>=mid||vis[a[i]])
{
st.insert(a[i]);
}
else
{
vis[a[i]]=1;
}
}
for(int i=0;i<mid;i++)
{
if(vis[i])
{
continue;
}
auto iter=st.upper_bound(2*i);
if(iter==st.end())
{
return false;
}
st.erase(iter);
}
return true;
};
int l=0;
int r=mx+1;
int m;
int ans;
while(l<=r)
{
m=l+r>>1;
if(check(m))
{
ans=m;
l=m+1;
}
else
{
r=m-1;
}
}
cout<<ans<<endl;
}
void init()
{
}
signed main()
{
ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
int t=1;
cin>>t;
init();
while(t--)
{
solve();
}
return 0;
}
因为 MEX 具有单调性,所以可以二分答案,那么就是每次检查能否冲到 mid 这个数。那么对于大于等于 mid 的数就都可以作为候选数,而对于小于 mid 的数,肯定是直接贪心地让其保持不变更优。其中,这些数如果出现多次,那么也是可以作为候选数的。
对于候补数 x,考虑其能通过取模变成什么数。令其变成的数为 i,那么就有 。将
看作模数本身,那么式子就变为
。由于又要求
,那么就有
。所以对于候补数 x,其可以变成
。那么在从 0 到 mid-1 枚举判断时,对于没被填过的数 x,二分判断候补数中是否有大于
即可。
四、D. Reserved Reversals
太变态了这个题......
cpp
#include <bits/stdc++.h>
using namespace std;
/* /\_/\
* (= ._.)
* / > \>
*/
/*
*想好再写
*注意审题 注意特判
*不要红温 不要急躁 耐心一点
*WA了不要立马觉得是思路不对 先耐心找反例
*/
#define endl '\n'
#define dbg(x) cout<<#x<<endl;cout<<x<<endl;
#define vdbg(a) cout<<#a<<endl;for(auto x:a)cout<<x<<" ";cout<<endl;
#define YES cout<<"YES"<<endl;return ;
#define Yes cout<<"Yes"<<endl;return ;
#define NO cout<<"NO"<<endl;return ;
#define No cout<<"No"<<endl;return ;
#define popcount __builtin_popcount
using ll=long long;
using ld=long double;
using pii=pair<int,int>;
using pll=pair<ll,ll>;
const int INF=1e9;
const ll INFLL=1e18;
const int dx[]={-1,1,0,0};
const int dy[]={0,0,-1,1};
const int ddx[]={-2,-1,1,2,2,1,-1,-2};
const int ddy[]={1,2,2,1,-1,-2,-2,-1};
void solve()
{
int n;
cin>>n;
vector<int>a(n+1);
for(int i=1;i<=n;i++)
{
cin>>a[i];
}
vector<int>vmax(2);
vector<int>vmin(2,INF);
for(int i=1;i<=n;i++)
{
int p=a[i]%2;
vmax[p]=max(vmax[p],a[i]);
vmin[p]=min(vmin[p],a[i]);
}
auto check=[&](int p)->bool
{
int cur=0;
for(int i=1;i<=n;i++)
{
if(a[i]%2!=p)
{
continue;
}
if(cur>a[i])
{
if(vmax[p^1]<cur&&vmin[p^1]>a[i])
{
return false;
}
}
cur=max(cur,a[i]);
}
return true;
};
if(check(0)&&check(1))
{
YES;
}
NO;
}
void init()
{
}
signed main()
{
ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
int t=1;
cin>>t;
init();
while(t--)
{
solve();
}
return 0;
}
首先,不难想到若相邻两数一奇一偶,那么肯定是可以任意交换的。那么对于一个数,其肯定可以通过交换穿过一个所有数奇偶性均与其不同的区间。
由这点得到启发,开始对脑电波。考虑先让奇数和偶数分开,然后让其分别有序。那么最后再像归并排序一样,把两部分整合到一起即可。
那么对于奇偶性相同的两数 (x,y),其不合法当且仅当其构成一个逆序对。那么当出现这种情况,此时就需要拉过来一个奇偶性和这两数不同的数 c,使得 c 是三者的最大或最小值。只有这样才可以通过 c 把 x,y 交换过来。
由于逆序对很多,发现可以不用每个都判断。在分别枚举奇偶数的时候,对于当前数字 v,只需要判断其和前缀最大值是否构成了逆序对。如果构成了就直接拿奇偶性不同的数中的最大最小值判断即可。
具体的构造策略是,在分开奇偶块以后,认为左侧全奇数右侧全偶数。对于当前的奇数逆序对 (x,y),考虑将被这两数包围的奇数区间全移动到右侧最值 c 的前面。然后翻转这个区间,消除 (x,y) 这个逆序对,然后再移动回来,之后重复即可。
五、E. Mental Monumental (Hard Version)
cpp
#include <bits/stdc++.h>
using namespace std;
/* /\_/\
* (= ._.)
* / > \>
*/
/*
*想好再写
*注意审题 注意特判
*不要红温 不要急躁 耐心一点
*WA了不要立马觉得是思路不对 先耐心找反例
*/
#define endl '\n'
#define dbg(x) cout<<#x<<endl;cout<<x<<endl;
#define vdbg(a) cout<<#a<<endl;for(auto x:a)cout<<x<<" ";cout<<endl;
#define YES cout<<"YES"<<endl;return ;
#define Yes cout<<"Yes"<<endl;return ;
#define NO cout<<"NO"<<endl;return ;
#define No cout<<"No"<<endl;return ;
#define popcount __builtin_popcount
using ll=long long;
using i128=__int128;
using ld=long double;
using pii=pair<int,int>;
using pll=pair<ll,ll>;
const int INF=1e9;
const ll INFLL=1e18;
const int dx[]={-1,1,0,0};
const int dy[]={0,0,-1,1};
const int ddx[]={-2,-1,1,2,2,1,-1,-2};
const int ddy[]={1,2,2,1,-1,-2,-2,-1};
template<typename T>
struct Segment_Tree
{
vector<T>data;
//自定义
vector<T>info;
Segment_Tree(){}
Segment_Tree(int n){
data.assign(n<<2,0);
info.assign(n<<2,0);
}
void add(int jobl,int jobr,T jobv,int l,int r,int i){
if(jobl<=l&&r<=jobr){
addLazy(i,jobv);
}
else{
int m=(l+r)>>1;
down(i);
if(jobl<=m){
add(jobl,jobr,jobv,l,m,i<<1);
}
if(m+1<=jobr){
add(jobl,jobr,jobv,m+1,r,i<<1|1);
}
up(i);
}
}
T query(int jobl,int jobr,int l,int r,int i){
if(jobl<=l&&r<=jobr){
return data[i];
}
int m=(l+r)>>1;
down(i);
//自定义
T ans=INFLL;
if(jobl<=m){
ans=min(ans,query(jobl,jobr,l,m,i<<1));
}
if(m+1<=jobr){
ans=min(ans,query(jobl,jobr,m+1,r,i<<1|1));
}
return ans;
}
//自定义
void addLazy(int i,T v){
data[i]+=v;
info[i]+=v;
}
//自定义
void up(int i){
data[i]=min(data[i<<1],data[i<<1|1]);
}
//自定义
void down(int i){
if(info[i])
{
addLazy(i<<1,info[i]);
addLazy(i<<1|1,info[i]);
info[i]=0;
}
}
};
void solve()
{
int n;
cin>>n;
vector<int>a(n+1);
int mx=-1;
for(int i=1;i<=n;i++)
{
cin>>a[i];
mx=max(mx,a[i]);
}
int m=mx+1;
Segment_Tree<ll>st(m+1);
vector<int>vis(m+1);
vector<int>cnts(m+1);
int cur=0;
for(int i=1;i<=n;i++)
{
//a[i]
if(!vis[a[i]]&&a[i]<=cur)
{
vis[a[i]]=1;
st.add(0,a[i],1,0,m,1);
}
//candidate
else
{
cnts[a[i]]++;
int L=(a[i]-1)/2;
if(L>=0)
{
st.add(0,L,1,0,m,1);
}
}
//push ans
while(1)
{
//pick a[p]=cur
if(cnts[cur]&&!vis[cur])
{
int L=(cur-1)/2;
if(L>=0)
{
st.add(0,L,-1,0,m,1);
}
if(st.query(0,m,0,m,1)>=0)
{
vis[cur]=1;
cur++;
}
else
{
if(L>=0)
{
st.add(0,L,1,0,m,1);
}
break;
}
}
//use candidate
else
{
st.add(0,cur,-1,0,m,1);
if(st.query(0,m,0,m,1)>=0)
{
cur++;
}
else
{
st.add(0,cur,1,0,m,1);
break;
}
}
}
cout<<cur<<" ";
}
cout<<endl;
}
void init()
{
}
signed main()
{
ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
int t=1;
cin>>t;
init();
while(t--)
{
solve();
}
return 0;
}
首先,根据 Easy Version 的结论,对于一个数 ,其能负责的只有
和
本身。那么还是一样的贪心策略,如果
且
这个位置目前还是空缺的,那么就让其知道占用单点位置。否则就可以让其变小,作为候补元素。
由于要输出每个位置的答案,且这个答案存在单调性,所以可以考虑维护一个答案指针 ans,每次看加入一个数后能否扩出去。那么首先,对于当前要填入的数字 ans,若前面存在某个 ,那么肯定是直接拿过来用最优,因为让更大的数去填这个空只会更困难!否则的话,才会考虑是否能用候补的数填上所有空缺的数。
要想解决这个问题,需要引入二分图匹配和 Hall 定理。
首先,图匹配是指从图中选出一些边,使得没有任何两条线连到同一个点上。之后,Hall 定理在二分图匹配中的应用为,给定一个二分图,判断某侧的所有点能否都被匹配上。对于定理的内容,首先可以理解为,在男女生相亲的二分图问题中,若有 K 个男生,他们喜欢的女生总数小于 K,那么就不可能完成匹配。那么推广一下就是,对于二分图 (U,V),集合 U 能被完全匹配的充要条件是,对于集合 U 的任意子集 S,与 S 相连的 V 中的节点个数大于等于 S 的大小。
那么在这个题中,考虑将 [0,ans) 范围内缺失的数字看作集合 U,将所有候补数看作集合 V。然后对于每个候补数 x,向范围 内的所有缺失的数连边。那么问题就变为,缺失的数能否被完全覆盖。
肯定不能枚举子集,考虑进行优化。举个例子可以发现,对于集合 {2,5},此时要考虑的候补数的能力就需要 。那么当集合变为 {2,3,4,5} 时,可以发现要考虑的候补数的能力还是
。那么也就是说,根本没有必要枚举元素不连续的子集,直接考虑所有后缀即可。所以定义候补数的能力
,之后枚举每个后缀,要求候补数中
的个数大于等于
的空缺数个数。
考虑定义后缀数组 为能力
的候补数个数,减去
的空缺数个数的差值,那么能完全匹配就等价于对于任意后缀数组都有
。那么增加或删除一个能力为 L 的候补数,就等价于对区间 [0,L] 范围
,添加或减少一个空缺数 x,就等价于对区间 [0,x] 范围
。由于还需要判断是否每个位置的值都大于等于 0,所以还需要范围查最小值,那么就可以用线段树维护了。
已经尽可能说的清楚了,感觉嘴好笨不会讲题呜呜......中间枚举后缀那块感觉很抽象啊,已经想不到什么更好的解释方法了qwq......
总结
这个 E 太变态了,或者说这一整场都很变态......