有趣的事实:大多数题目的代码都比题面要短。
A - Ancient Civilization
题意
给你\(n\)个在二进制下有\(l\)位的数字,构造一个数字使得所有\(n\)个数字在看作字符串的时候到它的汉明距离的和最小。
做法
首先观察出答案的不同二进制位的取值互不影响,然后对于答案的每一位枚举它是\(0\)或\(1\)并选择代价更小的那一个。代价的定义是给定的\(n\)个数字中,对应位置不同的数字的个数。
程序
#include<bits/stdc++.h>
using namespace std;
int n,l,x[105];
void mian(){
cin>>n>>l;
for(int i=0;i<n;i++)cin>>x[i];
int ans=0;
for(int i=0;i<l;i++){
int cnt=0;
for(int j=0;j<n;j++)cnt+=x[j]>>i&1;
if(cnt>n/2)ans|=1<<i;
}
cout<<ans<<'\n';
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int T;
cin>>T;
while(T--)mian();
return 0;
}
B - Elementary Particles
题意
给定长度\(n\)的序列\(a\),假设有一数字\(k\),若能找到任意两个长度为\(k\)的连续子序列\(a_{p\dots p+k-1}\)和\(a_{q\dots q+k-1}\),使得存在一个位置\(i, 0\le i<k, a_{p+i} = a_{q+i}\),则该数字合法。找到最大的合法的\(k\)。
做法
首先可以考虑固定一个数字\(V\),使得找到的两个等长连续子序列中有一个相等的位置的值为\(V\)。
记\(V\)在\(a\)中出现的位置为\(b_1,b_2,\dots b_m\),我们可以枚举两个出现位置,然后计算包含它们的最长连续子段能够多长,并更新答案。
但是这样做仍会超时,注意到枚举的两个\(V\)的出现位置中假如中间还有一个\(V\)的出现位置,那么把枚举的其中一个换成中间的答案肯定更优,所以只需要枚举相邻的两个出现位置即可,此时复杂度可以通过此题。
程序
#include<bits/stdc++.h>
using namespace std;
int n,a[150005];
vector<int> b[150005];
void mian(){
for(int i=1;i<=150000;i++)b[i].clear();
cin>>n;
for(int i=1;i<=n;i++){
cin>>a[i];
b[a[i]].emplace_back(i);
}
int ans=-1;
for(int i=1;i<=150000;i++){
for(int j=0;j+1<b[i].size();j++){
int l=b[i][j],r=b[i][j+1];
ans=max(ans,l-1+n-r+1);
}
}
cout<<ans<<'\n';
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int T;
cin>>T;
while(T--)mian();
return 0;
}
C - Road Optimization
题意
一条路上有\(n\)个限速标牌,\(a_i\)表示从经过这个标牌开始到经过下一个标牌或者结束走完这条路为止,经过一单位距离需要\(a_i\)单位时间。\(d_i\)为标牌的位置,第一个标牌一定在道路起始。
你可以最多拆除\(k\)个标牌,要求最小化走完整条道路的时间。特别地,第一个标牌不能拆除。
做法
考虑DP。一种显然的转移方式是记录目前走到的位置和拆除的标牌数量,但是假如记录已经走过的标牌数量时,当前行走速度会被之前的拆除方案限制,所以不可行(或者可以多记录一维状态?我没细想)。
我们的状态可以设定为:\(f_{i,j}\)表示从第\(i\)个标牌开始走,拆除\(j\)个标牌(不能拆第\(i\)个)的方案数。转移时枚举从当前标牌开始,拆除了几个标牌后到下一个未拆除的标牌即可包括所有方案。
程序
#include<bits/stdc++.h>
using namespace std;
int n,k,d[505],a[505],f[505][505];
int main(){
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
cin>>n>>d[n+1]>>k;
for(int i=1;i<=n;i++)cin>>d[i];
for(int i=1;i<=n;i++)cin>>a[i];
memset(f,0x3f,sizeof(f));
f[n+1][0]=0;
for(int i=n;i>=1;i--){
for(int j=0;j<=n-i;j++){
for(int k=0;k<=j;k++){
f[i][j]=min(f[i][j],f[i+k+1][j-k]+a[i]*(d[i+k+1]-d[i]));
}
}
}
cout<<*min_element(f[1],f[1]+k+1);
return 0;
}
D - Binary Spiders
Binary Spiders -> BS -> Binary Search -> learn bs
这题和以前某一个模拟赛的题好像啊,可是我当时没有补,导致比同学切得慢好多(论好好补题的重要性)。
题意
给你\(n\)个数\(a_i\),选出最大的集合使得两两异或的结果不小于给定的\(k\)。在最大的集合大小小于\(2\)时输出\(-1\)。
做法
这类题目的一个套路是从高位到低位一位位考虑。
首先我们看出两个数字假如异或结果的二进制为\(1\)的最高位比\(k\)二进制为\(1\)的最高位还要高那么一定合法。于是我们可以初步按照这些比\(k\)最高非零位高的位置把数字分类,接下来假设处理的数字在这些高位上都相同。
我们可以先把数字集合按照当前二进制位取值分成\(01\)两类。由于第一个开始考虑的二进制位,\(k\)的对应位置一定是\(1\),那么我们已经只能从两个集合各最多选取一个数字,否则同集合的异或值一定小于\(k\)。
假如\(k\)的下一个二进制位(低一位)是\(1\),那么我们可以再把两个集合按照低一位的值分类,然后把这四个集合中下一位异或值为\(1\)的两对递归处理。
假如是\(0\),那么我们没有必要把两个集合分类,在当前的两个集合中任意选择都是可以的。特别地,我们可以类似上面的把集合再分类的方式,处理出四个集合,此时异或值为\(1\)的两对之间异或的结果已经比\(k\)大了,可以直接选择然后结束递归。
虽然每一层的递归函数数量是指数级别递增的,但是由于集合总大小是恒定的,所以每一层最多只有\(n\)个递归函数,复杂度是可以保证的。
程序
#include<bits/stdc++.h>
using namespace std;
int n,k,a[300005];
vector<int> ans;
bool sol(const vector<int> &lo,const vector<int> &hi,int h){
if(lo.empty())return false;
if(hi.empty())return false;
if(!h){
ans.emplace_back(lo.front());
ans.emplace_back(hi.front());
return true;
}
vector<int> ll[2],hh[2];
for(int x:lo)ll[a[x]>>h-1&1].emplace_back(x);
for(int x:hi)hh[a[x]>>h-1&1].emplace_back(x);
if(k>>h-1&1){
return sol(ll[0],hh[1],h-1)||sol(ll[1],hh[0],h-1);
}else{
if(!ll[0].empty()&&!hh[1].empty()){
ans.emplace_back(ll[0].front());
ans.emplace_back(hh[1].front());
return true;
}
if(!ll[1].empty()&&!hh[0].empty()){
ans.emplace_back(ll[1].front());
ans.emplace_back(hh[0].front());
return true;
}
return sol(lo,hi,h-1);
}
}
void deal(const vector<int> &v,int h){
if(v.empty())return;
vector<int> lo,hi;
for(int x:v)(a[x]>>h&1?hi:lo).emplace_back(x);
if(k>>h&1){
if(!sol(lo,hi,h)){
if(!lo.empty())ans.emplace_back(lo.front());
else if(!hi.empty())ans.emplace_back(hi.front());
}
}else{
deal(lo,h-1);
deal(hi,h-1);
}
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
cin>>n>>k;
for(int i=1;i<=n;i++){
cin>>a[i];
}
vector<int> d(n);
iota(d.begin(),d.end(),1);
if(k)deal(d,29);
else ans=d;
sort(ans.begin(),ans.end());
if(ans.size()==1){
cout<<-1;
return 0;
}
cout<<ans.size()<<'\n';
for(int x:ans)cout<<x<<' ';
return 0;
}
E1 - Cats on the Upgrade (easy version)
题意
给定一个不一定合法的括号串,每次查询它的一个合法括号子串,问这个子串有多少个非空合法括号子串。
做法
首先一个不合法的括号串当中的合法括号子串也一定是唯一确定的。我们考虑对合法括号子串建立括号树,特别地,建立一个超级根结点把所有极大合法括号子串连上去(E1中实际不需要维护这种树形结构,不过这样叙述更方便,也方便做E2)。
我们考虑一个括号树上结点,假如查询正好是查询了这个结点(而不是一些连续的兄弟结点),它的答案(记作\(f(u)\))是什么:
有一个事实:横跨两个连续的合法括号子串的合法子串只能是它们两个相互连接的结果。
基于这个事实,上面公式解释为,每个结点的答案,首先是所有儿子的答案(选择单独儿子贡献答案,或者选择儿子的子串贡献到答案里),或选择连续的一段儿子贡献答案,或选择这个结点对应的括号本身。
查询时,假如是查询了连续的几个括号树上的兄弟结点,那么求出\(\sum f(u)\)和总共的兄弟个数,类似上面的公式计算答案即可。
\(f(u)\)的统计可以不递归统计,对于每个结点只计算\(\binom {|son(u)|} 2 - |son(u)| + 1\)然后求答案的时候跑前缀和,贡献就是子树对应的括号串上子区间。
程序
E1的代码比较丑,建议直接去看E2的。
#include<bits/stdc++.h>
using namespace std;
int n,q,p[300005],vot[300005];
long long pre[300005];
char s[300005];
stack<int> t;
void deal(int l,int r){
int cnt=0,x=l+1;
while(x!=r){
cnt++;
x=p[x]+1;
}
if(!vot[l]){
vot[l]=vot[r]=1;
}
if(p[r+1]&&p[r+1]>r){
vot[r+1]=vot[p[r+1]]=vot[l]+1;
}
pre[l]=1+cnt*(cnt+1ll)/2-cnt;
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
cin>>n>>q>>s+1;
for(int i=1;i<=n;i++){
if(s[i]=='(')t.emplace(i);
else if(!t.empty()){
p[t.top()]=i;
p[i]=t.top();
t.pop();
}
}
for(int i=1;i<=n;i++)if(p[i]>i)deal(i,p[i]);
for(int i=1;i<=n;i++)pre[i]+=pre[i-1];
while(q--){
int l,r;
cin>>l>>l>>r;
int cnt=vot[r]-vot[l]+1;
cout<<pre[r]-pre[l-1]+(cnt+1ll)*cnt/2-cnt<<'\n';
}
return 0;
}
E2 - Cats on the Upgrade (hard version)
提示:操作中删除的括号对当中只有'.'这个字符(我赛时就没看到也可能是我太蠢)。
题意
和上一道题相比,我们需要删除一对相互匹配的括号了(不过保证删除的括号之间没有其他的括号对)。
做法
请先看E1的做法。
关键观察:删除操作实质是删除括号树上的一个叶子。
首先我们还是类似上一题的前缀和,考虑把结点对答案的贡献对应到区间上,子树内求和改为区间求和。删除一个叶子就是把它那个位置对答案的\(1\)的贡献删掉,然后给它的父亲结点的儿子个数减去\(1\)(同时修改父亲结点由于选择连续的儿子段产生的对答案的贡献)。
我们就需要维护一个单点修改,区间求和的线段树,来支持结点的贡献的修改操作。然后还要用一些数据结构来维护一个结点的儿子位置(由于可能有连续的多个儿子的查询,所以需要用它来求查询了几个连续的儿子结点)。这里可以使用平衡树,由于操作简单,可以直接使用pb_ds
提供的平衡树完成。然后记录每个结点的父亲,这是简单的。
程序
#include<bits/stdc++.h>
#include<ext/pb_ds/assoc_container.hpp>
#include<ext/pb_ds/tree_policy.hpp>
using namespace std;
using bst=__gnu_pbds::tree<int,__gnu_pbds::null_type,less<int>,__gnu_pbds::rb_tree_tag,__gnu_pbds::tree_order_statistics_node_update>;
typedef long long ll;
struct SegTree{
int sz;
vector<ll> dat;
SegTree(int _sz=1){
sz=1;while(sz<_sz)sz<<=1;
dat.resize(sz<<1);
}
void upd(int pos,ll val){
pos+=sz-1;
dat[pos]+=val;
while(pos>1){
pos>>=1;
dat[pos]=dat[pos<<1]+dat[pos<<1|1];
}
}
ll qry(int id,int l,int r,int ql,int qr){
if(qr<l||r<ql)return 0;
if(ql<=l&&r<=qr)return dat[id];
return qry(id<<1,l,l+r>>1,ql,qr)+qry(id<<1|1,(l+r>>1)+1,r,ql,qr);
}
ll qry(int l,int r){
return qry(1,1,sz,l,r);
}
}seg;
int n,p[300005],fa[300005];
char s[300005];
bst son[300005];
void deal(int l,int r){
int x=l+1;
while(x!=r){
fa[x]=l;
son[l].insert(x);
x=p[x]+1;
}
seg.upd(l,1+son[l].size()*(son[l].size()-1ll)/2);
}
void del(int l,int r){
seg.upd(l,-1);
ll cnt=son[fa[l]].size();
if(fa[l])seg.upd(fa[l],-cnt*(cnt-1)/2);
son[fa[l]].erase(l);
if(fa[l])seg.upd(fa[l],(cnt-1)*(cnt-2)/2);
}
ll ask(int l,int r){
ll res=seg.qry(l,r);
int cnt=son[fa[l]].order_of_key(r)-son[fa[l]].order_of_key(l);
return res+cnt*(cnt-1ll)/2;
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int q;
cin>>n>>q>>s+1;
seg=SegTree(n);
{
stack<int> t;
for(int i=1;i<=n;i++){
if(s[i]=='(')t.emplace(i);
else if(!t.empty()){
p[t.top()]=i;
p[i]=t.top();
t.pop();
}
}
}
memset(fa,-1,sizeof(fa));
for(int i=1;i<=n;i++)if(p[i]>i){
if(fa[i]==-1)son[fa[i]=0].insert(i);
deal(i,p[i]);
}
while(q--){
int t,l,r;
cin>>t>>l>>r;
if(t==1){
del(l,r);
}else{
cout<<ask(l,r)<<'\n';
}
}
return 0;
}