A. Stickers and Toys
http://codeforces.com/contest/1187/problem/A
簽到題,給s個便籤,t個玩具,塞進n個蛋裏(暗示奇趣蛋?),保證沒有蛋是空的,問至少取幾個蛋可以保證得到至少一個便籤和一個玩具。
因爲沒有蛋是空的,所以可以算出有s+t-n個蛋裏兩種都有,n-s個蛋裏只有玩具,n-t個蛋裏只有便籤。所以最壞情況是取了n-s個玩具,或者n-t個便籤,再之後隨便取什麼都能滿足條件,所以直接輸出。
複雜度O(1)
B. Letters Shop
http://codeforces.com/contest/1187/problem/B
給一個字符串s,接下來m組詢問,問字符串每組給定一個字符串t,如果取s的前綴來重組成t(可以過量),問至少要取幾位前綴。數據保證有解。
這個問題滿足單調性,且全取比可行,取0個必不可行,以0和s的長度n爲左右指針二分,判斷mid=(l+r)>>1位置處s的前綴中各個字母的數字是否都大於t各個字母的個數,先預處理前綴和數組,就可以在O(m*26log(n))時間內完成。
但是其實還有更快的做法,直接保存s中每個字母各個字母第i次出現的位置假定t中各個字母個數爲 ,則結果 ,複雜度O(n+m)。
以下給出二分代碼,因爲做題的時候我想的就是二分。
#include<bits/stdc++.h>
using namespace std;
int n,m;
string s;
int cnt[26][200005];
int c[26];
bool check(int x){
for(int i=0;i<26;++i){
if(cnt[i][x]<c[i])return 0;
}
return 1;
}
int main(){
cin>>n>>s;
for(int i=1;i<=n;++i){
for(int j=0;j<26;++j)cnt[j][i]=cnt[j][i-1];
++cnt[s[i-1]-'a'][i];
}
cin>>m;
while(m--){
cin>>s;
memset(c,0,sizeof(c));
for(auto x:s)++c[x-'a'];
int l=0,r=n;
while(l<r-1){
int m=(l+r)>>1;
if(check(m))r=m;
else l=m;
}
cout<<r<<endl;
}
}
C. Vasya And Array
http://codeforces.com/contest/1187/problem/C
構造題。給你一些區間,其中一些區間要求單調不減,另一些要求不滿足單調不減,也就是區間裏存在滿足。
顯然上述兩個是事件是對立的,對於任意一段區間,這兩個事件是其可能狀態的一個分割。且單調不減的結論要比他的對立事件強很多,因爲他要滿足任意性,而其對立事件只要滿足存在性。所以我們考慮先滿足所有的單調不減調節,再判斷能否滿足其對立條件。
我們保存一個數組b,表示,反之取0 。對於所有輸入的結論,若,則表示其左端點到右端點之間滿足單調不減性,將到均置爲1 。理論上這個操作可以用線段樹完成以實現nlogn的複雜度,但是考慮到數據不大,這裏直接用線性表存儲即可。而結束之後,構造這樣一個數組a:
最後檢驗所有的區間裏是否至少存在一個b==0即可,也可用線段樹快速實現檢驗。
事實上這道題可以考慮改編成一個區間染色+區間查詢元素存在性的題目,作爲這道題的數據加強版,考察線段樹的使用。
以下給出本題代碼:
#include<bits/stdc++.h>
using namespace std;
int a[1005];
int c[1005];
vector<pair<int,int> >b;
bool check(int l,int r){
for(int i=l;i<r;++i){
if(a[i]==0)return 1;
}
return 0;
}
int main(){
int n,m,t,l,r;
cin>>n>>m;
while(m--){
cin>>t>>l>>r;
if(t){
for(int i=l;i<r;++i)a[i]=1;
}else{
b.push_back(make_pair(l,r));
}
}
for(auto x:b){
l=x.first,r=x.second;
if(check(l,r)==0){
cout<<"NO"<<endl;
return 0;
}
}
cout<<"YES"<<endl;
c[0]=n;
for(int i=1;i<n;++i){
if(a[i])c[i]=c[i-1];
else c[i]=c[i-1]-1;
}
for(int i=0;i<n;++i)cout<<c[i]<<" ";
cout<<endl;
return 0;
}
D. Subarray Sorting
http://codeforces.com/contest/1187/problem/D
這題是真實線段樹題。
問題是給你一個數組,再給你另一個,你每次可以對第一個數組做一次區間排序,問能否變成第二個數組。
首先,每個區間排序,一定可以被有限個二元排序替換。所有我們不妨把操作從區間排序變成相鄰的二元排序(冒泡排序的思路)。事實上這題也很像是冒泡排序,你要把你需要的元素一路冒泡的你需要的位置。
那麼我們一步步來,先考慮簡單的情況。先考慮最後一個元素,如果這兩個元素相同,那麼可以直接不管他,往左移動一格,考慮一個規模更小的子問題。如果不同,那麼我們希望第一個數組中,在不影響其他元素的相對順序的情況下,從左邊調一個需要的元素過來(這不就是冒泡嗎?) 。如果能做到,那麼就轉換成了上一種情況,而如果做不到,那麼說明這個問題無解。
這樣我們就把問題轉換成了一個判斷問題:能否從左邊調一個元素在不影響原數組順序的情況下到最右邊。首先,考慮冒泡排序的特性,其他元素本來就不會受到影響,所有這個條件直接掠過。接下來只要判斷能否移過去就行。這是非常顯然的,如果在這段區間內有大於他的元素,那麼就過不去,反之一定過得去。
如何判斷?維護線段樹,保存區間最大值,然後每次都找當前位置往左首次出現大於等於當前元素的位置。如果找到的值大於當前元素,那麼就算有這個元素,他也會被擋住,所以返回NO;若找不到,說明根本不存在這個元素,返回NO;若找到且恰好等於,那麼我們要的就是這樣元素。但是我們模擬把他挪到右邊太麻煩了, 所以我們直接把他置爲0,表示已經被取過,且不會影響線段樹維護的區間最值的性質。而因爲我們要找的元素的閾值大於等於1,所以無論如何不會被取到,於是可以實現。
複雜度O(nlogn)
#include<bits/stdc++.h>
using namespace std;
int t[300005<<2];
int a[300005];
int b[300005];
void build(int o,int l,int r){
if(l==r)t[o]=a[l];
else{
int m=(l+r)>>1;
build(o<<1,l,m);
build(o<<1|1,m+1,r);
t[o]=max(t[o<<1],t[o<<1|1]);
}
}
void modify(int o,int l,int r,int x){
if(l==r)t[o]=a[l]=0;
else{
int m=(l+r)>>1;
if(x<=m)modify(o<<1,l,m,x);
else modify(o<<1|1,m+1,r,x);
t[o]=max(t[o<<1],t[o<<1|1]);
}
}
int query(int o,int l,int r,int x){
if(t[o]<x)return -1;
if(l==r)return l;
int m=(l+r)>>1;
int ret=query(o<<1|1,m+1,r,x);
if(~ret)return ret;
return query(o<<1,l,m,x);
}
int T,n;
int main(){
cin>>T;
while(T--){
cin>>n;
for(int i=1;i<=n;++i)scanf("%d",&a[i]);
build(1,1,n);
for(int i=1;i<=n;++i)scanf("%d",&b[i]);
int flag=1;
for(int i=n;i;--i){
int q=query(1,1,n,b[i]);
if(q==-1||a[q]!=b[i]){
flag=0;
break;
}
else modify(1,1,n,q);
}
cout<<(flag?"YES":"NO")<<endl;
}
}
E. Tree Painting
http://codeforces.com/contest/1187/problem/E
出題人號稱本題是旋根入門題emmmm結果發現有別的解法導致本題過題數遠大於上面一題。
題面是給你一棵樹,每次選擇一個相鄰的節點,然後得到等同於當前節點所在連通塊裏節點個數的分數,然後把這個節點殺死。問最多得到多少分。第一個節點可以任選。
事實上考慮第一個節點選定以後,結果是唯一確定的。所以問題就是要選定一個根節點,使得答案最大。這個答案的結果有如下兩種表示:
1.所有節點到根節點的距離之和。
2.所有節點作爲子樹根節點得到的樹的size的和。
這兩個問題似乎是對偶問題,一般而言第一個問題似乎更具實際意義一點。
先考慮簡單一點的情況,如果我們已經選定了根節點,如何計算?
不妨使用樹形dp,保存兩個數組:sz[i]表示以i爲根的子樹的規模;dp[i]表示以i爲根的子樹上求出的答案。
有如下轉移方程(考慮第二個問題的表述):
其中ch(i)表示i的孩子節點的集合。
兩邊dfs即可求出值。接下來介紹reroot操作。
觀察方程,會發現如果我們把根節點從i轉移到j上,事實上會發生改變的數值只有。只要手動修改這幾個數值,就能實現根節點的轉移,如此做一遍dfs,保存最大值,就是我們要的答案。考慮父節點爲u,子節點爲v.
變形如下:
因爲dp依賴於sz,將v從u上剪下來,所以有:
整理得:
相應的,將u接到v的孩子的位置,v有類似的變換:
整理得:
得到核心代碼段:
void dfs3(LL u,LL fa){
ans=max(dp[u],ans);
for(auto v:g[u]){
if(v==fa)continue;
dp[u]-=dp[v];
dp[u]-=sz[v];
sz[u]-=sz[v];
sz[v]+=sz[u];
dp[v]+=dp[u];
dp[v]+=sz[u];
dfs3(v,u);
dp[v]-=sz[u];
dp[v]-=dp[u];
sz[v]-=sz[u];
sz[u]+=sz[v];
dp[u]+=sz[v];
dp[u]+=dp[v];
}
return;
}
這就是其核心操作,一族關於父子節點的狀態參數的變換方程及其逆變換以實現根節點的旋轉的效果。其實很有平衡樹的感覺。
但是其使用條件也比較明顯,就算旋根時改變狀態參量的節點不多,比方說本題中是由父子節點受到影響,所以才能以一個常數的代價實現本操作。其複雜度還是dfs的複雜度。
最終複雜度O(n)。
給出完整代碼:
#include<bits/stdc++.h>
using namespace std;
#define LL long long
LL n;
vector<LL> g[200005];
LL dp[200005],sz[200005];
LL ans;
LL dfs1(LL u,LL fa){
sz[u]=1;
for(auto v:g[u]){
if(v==fa)continue;
sz[u]+=dfs1(v,u);
}
return sz[u];
}
LL dfs2(LL u,LL fa){
dp[u]=sz[u];
for(auto v:g[u]){
if(v==fa)continue;
dp[u]+=dfs2(v,u);
}
return dp[u];
}
void dfs3(LL u,LL fa){
ans=max(dp[u],ans);
for(auto v:g[u]){
if(v==fa)continue;
dp[u]-=dp[v];
dp[u]-=sz[v];
sz[u]-=sz[v];
sz[v]+=sz[u];
dp[v]+=dp[u];
dp[v]+=sz[u];
dfs3(v,u);
dp[v]-=sz[u];
dp[v]-=dp[u];
sz[v]-=sz[u];
sz[u]+=sz[v];
dp[u]+=sz[v];
dp[u]+=dp[v];
}
return;
}
int main(){
scanf("%lld",&n);
for(LL i=1,x,y;i<n;++i){
scanf("%lld%lld",&x,&y);
g[x].push_back(y);
g[y].push_back(x);
}
dfs1(1,-1);
ans=dfs2(1,-1);
dfs3(1,-1);
cout<<ans<<endl;
}