有趣的事實:大多數題目的代碼都比題面要短。
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;
}