【洛谷】NOIP2018原創模擬賽DAY2題解
前言:
我相信大家可以感覺到DAY2題目的難度明顯比DAY1大很多,這也是近年NOIP考試的趨勢,從目前NOIP考察的知識來看這次的T3知識可能對NOIP選手來說略難,但說不定今年NOIP還會考更高階的演算法,所以要有所防備。
再說一點,大家一定要注意部分分的獲取。對於這套題,如果能力一般的同學採用部分分演算法,理論至少上可以得到:100(預處理+動態規劃)+40(只解決純串聯或並聯情況)+55(採用暴力的“逐步爬山法”計算)=195分,對於DAY2來說是一個不錯的分數。對於T2,個人覺得細節相當多,而且不知道“標程”是否正確。對於T3,得部分分難度不大,但它就是一個防NOIP選手AK的題,可以說,對大部分人(包括我)來說這道題寫正解(200+ 行程式碼)還不如暴力(約100行程式碼)來得快。
最後,不要認為出題人能力比其他人強。因為出題沒有比賽那麼短的時間限制,所以可以花更多時間來解決自己的題。比如對於T2的第一個和第二個版本我自己都做不來,於是改為了現在的第三個版本,而且“標程”寫錯兩次;對於T3我自己寫了大約3個小時才寫完正解。所以這次比賽200+的同學都是能力在我之上的。 ——於2018/10/14
當然題目方法不唯一,如果大家對DAY2的題目有更好的解法,歡迎提出!
T1:最後的戰役
考察知識:map,動態規劃
演算法難度:XXX+ 實現難度:XXX
說實話,我動態規劃很弱,所以就出了一道不那麼難的動態規劃。
這道題貪心並不是完美解法,但是資料為隨機生成的,所以得分概率非常大。
Hack資料:
4 2
1 1
2 3
3 6
4 8
1 2 3 4
標程輸出:22 貪心輸出:21
分析:
首先,我們要解決操作2,如果暴力列舉時間複雜度為。
其實吧,我們直接用map優化就可以了,時間複雜度:
for(int i=1;i<=n;i++) P[i]=max(P[i-1],p[i]);//求最大值 for(int i=1;i<=n;i++){ mp[k[i]]+=p[i];//在這裡求和 P[i]=max(mp[x[i]],P[i]); }
在處理了伏地魔在 [1,n] 每一步可以獲得的最大魔法能量之後(記為P [ i ],記 為 SUM),我們就可以採取動態規劃了:
定義:表示在[ 1 , i ] 秒中在 i 秒使用了魔法,且有 j 個時間段使用了魔法,可以得到的最大能量值
邊界:
狀態轉移:
狀態轉移方程的實現還是要稍微處理一下,直接實現會超時:
for(int i=1;i<=n;i++)//處理邊界
f[i][1]=SUM-P[i]+P[i+1],
ans=max(ans,f[i][1]);
for(int j=2;j<=m;j++){
for(int k=1;k<=n;k++) T[k]=max(T[k-1],f[k][j-1]);//預處理
for(int i=2*j-1;i<=n;i++)
f[i][j]=T[i-2]-P[i]+P[i+1],
ans=max(ans,f[i][j]);
}
好了,這道題我們就做出來了,理論時間複雜度:,但是時間效率不是很高,面對最大資料需要近900ms。
T2:流量計算
考察知識:圖論,數學,推導,搜尋
演算法難度:XXXX 實現難度:XXXX
出題背景:我目前在學高中電學。最開始這道題搞得很難,我自己都做不來,於是不斷降低難度:從混連->並聯與並聯和串聯巢狀->並聯只能巢狀串聯。
分析:
這道題需要大家有一定的電學知識和數學推導能力。
先推導幾個電學結論:
結論一:對於串聯電路,我們可以將所有電阻看作一個等效電阻:
證明:易得,略
結論二:對於一個(等效)並聯電路,電阻分別為等效電阻為:
證明:設電壓為 U,由歐姆定律,總電流為:,故等效電阻為:
結論三:對於一個(等效)並聯電路,電阻分別為,我們可以採取下面的演算法計算等效電阻:
double R_=R[1];
for(int i=2;i<=n;i++) R_=R_*R[i]/(R_+R[i]);
ans=R_;
證明:結論二的二維形式:,每次將兩個電阻合併為一個等效電阻,最後的值即為總等效電阻
結論四:對於一個(等效)並聯電路,電阻分別為,其支路最小電流為:
證明:我們知道,最小電流在電阻最大的支路上,即,而電壓為,由,就得到結論
有了這幾個結論,我們就可以開始動手了:
1.先用結論三處理重邊
2.然後bfs求出由電源正極到負極的一條路徑
3.以這條路徑為主線開始處理,遇到分叉邊就表明此處有並聯巢狀串聯,用結論一+dfs計算巢狀串聯的等效電阻,並記下,然後利用結論三進行並聯電阻的合併
4.將所有等效電阻求和,即為整個電路的總等效電阻,如果主線電流為 則最小電流
程式碼實現細節請參考程式碼
T3:PION字尾自動機
考察知識:樹鏈剖分,線段樹,連結串列,排序,二分,字串處理
演算法難度:XXXXX 實現難度:XXXXX
分析:難題,演算法複雜,程式碼量大!這是一道典型的考場上正解不如寫暴力的題!
先介紹暴力演算法(55分):
演算法難度:XXX 實現難度:XXX+
顯然儲存檔案字尾資訊不能用陣列,空間開不下,所以我們用連結串列儲存所有資料夾中檔案的字尾資訊;
然後是字尾的處理,注意到字串長度小於6,如果我們用 1 表示 a ,2 表示 b ,... ... ,26 表示 z,這就相當於將字串看做一個27進位制數,這樣我們就可以在 int 範圍內用數字表示字串了
解決了上面兩個問題之後,我們就可以建樹然後對每個操作用暴力的 “逐步爬山法” 解決了,實現難度不大
其實這種方法對隨機資料效果還是比較好的(甚至比下面的滿分演算法還略快一點),但是你覺得資料可能完全隨機麼?
時間複雜度:極端情況約
滿分演算法:
程式碼量比較大。
看到這道題我們應該可以想到用樹鏈剖分來做。
我們先考慮怎麼對序列進行統計和修改,對於操作2,我們需要找到一個序列中的子序列。我們可以用一個結構體來儲存,結構體儲存序列中每個元素的值(字串的hash值)和序列中每個元素在序列中的位置。然後將這個結構體序列按元素值的排序(元素值相同的按在序列中的位置排序)。當我們需要查詢一個序列相同數值所在的區間的時候用 lower_bound 和 upper_bound 就可以完成,對於查找出來的目標區間,還需要用二分法來查詢該區間中元素的下標在目標子序列中的數量。
至於修改,用線段樹的區間維護實現。
解決了對序列的修改,我們還要解決樹中每個節點含有的檔案數目不相同的問題,如果一個節點沒有檔案,那麼簡單的方法是新建一個偽檔案,可以將hash值賦為 -1。
之後就是樹鏈剖分了,不同於樹鏈剖分模板,我們還要新建一個輔助陣列 low[i] ,表示節點 i 的重兒子的重兒子...(以此類推)可以到達的最深節點的編號,我們先對排序,之後查詢的時候我們查詢 之間的序列即可。
其實吧,上面只是程式碼實現的一部分,具體細節遠不止這些,更多的細節請參考程式碼。
時間複雜度:極端情況約
程式碼:
(僅供參考)
T1:
#include<cstdio>
#include<cstring>
#include<map>
#include<algorithm>
using namespace std;
const int maxn=50005;
map<int,int>mp;
int k[maxn],p[maxn],x[maxn];
int n,m,P[maxn],f[maxn][505],T[maxn],sum[maxn],SUM;
void ready(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++) scanf("%d%d",k+i,p+i);
for(int i=1;i<=n;i++) scanf("%d",x+i);
for(int i=1;i<=n;i++) P[i]=max(P[i-1],p[i]);
for(int i=1;i<=n;i++){
mp[k[i]]+=p[i];//用map儲存即可
P[i]=max(mp[x[i]],P[i]);
}
}
void dp(){
/*
f(i,j)表示已i為結尾且有j個時間段使用了魔法,可以得到的最大能量值
f(i,j)=MAX{f(k,j-1)}-P[i]+P[i+1] 1<j<=i-2
*/
int ans;
for(int i=1;i<=n;i++) SUM+=P[i];
ans=SUM;
if(m==0) {printf("%d\n",ans);return;}
for(int i=1;i<=n;i++)
f[i][1]=SUM-P[i]+P[i+1],
ans=max(ans,f[i][1]);
for(int j=2;j<=m;j++){
for(int k=1;k<=n;k++) T[k]=max(T[k-1],f[k][j-1]);//類似於預處理
for(int i=2*j-1;i<=n;i++)
f[i][j]=T[i-2]-P[i]+P[i+1],
ans=max(ans,f[i][j]);
}
printf("%d\n",ans);
}
int main(){
ready();
dp();
return 0;
}
T2:
#include<map>
#include<queue>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int maxn=20005;
map<pair<int,int>,double>mp,mp2;
map<pair<int,int>,double>::iterator it;
struct edge{
int to,next;
double R;
}e[maxn*5];
int head[maxn],np;
void adde(int u,int v,double R){
e[++np]=(edge){v,head[u],R};
head[u]=np;
e[++np]=(edge){u,head[v],R};
head[v]=np;
}
int n,m,from,to;
bool vis[maxn],done1[maxn],done2[maxn];
int d[maxn],fa[maxn],son[maxn];
double U,k=1.0,R_sum,R_fa[maxn],T[maxn];
void build(){
int R,u,v;
char S[5];
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++){
scanf("%d%d%s%d",&u,&v,S,&R);
if(S[0]=='P') from=u,to=v,U=(double)R;//找到電源
else{
if(u>v) swap(u,v);
pair<int,int>pr=make_pair(u,v);
if(mp.count(pr)){//處理重邊構成的並聯
double R_=mp[pr];
mp[pr]=R_*R/(R+R_);//並聯電路電阻的計算
mp2[pr]=max(mp2[pr],(double)R);
}
else mp[pr]=mp2[pr]=(double)R;
}
}
for(it=mp.begin();it!=mp.end();it++){
pair<int,int>pr=it->first;
k=min(k,mp[pr]/mp2[pr]);//k的計算
adde(pr.first,pr.second,it->second);
}
}
void bfs(int s){//尋找路徑
memset(d,0x3f,sizeof(d));
memset(vis,0,sizeof(vis));
fa[s]=d[s]=0,vis[s]=true;
queue<int>q;
q.push(s);
while(!q.empty()){
int i=q.front();q.pop();
for(int p=head[i];p;p=e[p].next){
int j=e[p].to;
if(vis[j]) continue;
fa[j]=i,R_fa[j]=e[p].R;
vis[j]=true,d[j]=d[i]+1;
q.push(j);
}
}
}
int dfs(int i,double& sum){//尋找支路
for(int p=head[i];p;p=e[p].next){
int j=e[p].to;
if(done2[j]) continue;
sum+=e[p].R;
if(done1[j]) return j;//找到另一個交匯點
else {done2[j]=true;return dfs(j,sum);}
} return -1;
}
void solve(){
bfs(from);
for(int i=to;i;i=fa[i]) done1[i]=true,son[fa[i]]=i;//標記主線
for(int i=to;i!=from;i=fa[i]){
double R_max,R_=0;//R_:等效電阻
int cnt=0,pos;
for(int p=head[i];p;p=e[p].next){
int j=e[p].to;
if(done1[j]||done2[j]) continue;
T[++cnt]=mp[make_pair(min(i,j),max(j,i))];
done2[i]=done2[j]=true;//標記支線
pos=dfs(j,T[cnt]);
}
if(!cnt){R_sum+=R_fa[i];continue;}//當前路線不包含並聯
for(int j=i;j!=pos;j=fa[j]) R_+=R_fa[j];
R_max=R_;
for(int j=1;j<=cnt;j++){
R_max=max(R_max,T[j]);
R_=R_*T[j]/(R_+T[j]);
}
k=min(k,R_/R_max);
R_sum+=R_,i=son[pos];
}
printf("%.2lf\n%.2lf\n",U/R_sum,U*k/R_sum);
printf("%d",1/0);
}
int main(){
build();
solve();
return 0;
}
T3:(暴力演算法)
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int maxn=100005;
int next[maxn*5],hash[maxn*5],first[maxn],last[maxn],np_;
void add_file(int u,int num){//連結串列實現按位置儲存檔案
hash[++np_]=num;
if(first[u]) next[last[u]]=np_;
else first[u]=np_;
last[u]=np_;
}
int hash_num(char* s){
int ret=0;
for(int i=0;s[i]!=0;i++) ret=ret*27+s[i]-96;
return ret;
}
struct edge{//儲存樹邊
int to,next;
}e[maxn*2];
int head[maxn],np;
void adde(int u,int v){//新增樹邊
e[++np]=(edge){v,head[u]};
head[u]=np;
e[++np]=(edge){u,head[v]};
head[v]=np;
}
int n,m,k[maxn],dep[maxn],fa[maxn];
void dfs(int i,int Fa){
fa[i]=Fa,dep[i]=dep[Fa]+1;
for(int p=head[i];p;p=e[p].next){
int j=e[p].to;
if(j==Fa) continue;
dfs(j,i);
}
}
void build(){
int u,v;
char ext_nm[10];
scanf("%d%d",&n,&m);
for(int i=1;i<n;i++) scanf("%d%d",&u,&v),adde(u,v);
for(int i=1;i<=n;i++){
scanf("%d",k+i);
for(int j=1;j<=k[i];j++){
scanf("%s",ext_nm);
add_file(i,hash_num(ext_nm));//插入連結串列
}
}
dfs(1,0);
}
int lca(int x,int y){
while(x!=y){
if(dep[x]<dep[y]) y=fa[y];
else x=fa[x];
}
return x;
}
int query_path(int x,int y,int id,bool del){//暴力處理資料夾
int ret=0;
while(x!=y){
if(dep[x]<dep[y]) swap(x,y);
for(int p=first[x];p;p=next[p]) if(hash[p]==id){
ret++;
if(del) hash[p]=-1;
}
x=fa[x];
}
for(int p=first[x];p;p=next[p])if(hash[p]==id){
ret++;
if(del) hash[p]=-1;
}
return ret;
}
void solve(){
char cmd[3][10];
int u,v,id;
while(m--){
scanf("%s%s",cmd[0],cmd[1]);
if(cmd[0][0]=='q'){
if(cmd[1][1]=='p'){
scanf("%d%d",&u,&v);
printf("%d\n",dep[u]+dep[v]-2*dep[lca(u,v)]);
} else {
scanf("%d%d%s",&u,&v,cmd[2]);
id=hash_num(cmd[2]+2);
printf("%d\n",query_path(u,v,id,false));
}
}
else{
scanf("%d%d%s",&u,&v,cmd[2]);
id=hash_num(cmd[2]+2);
printf("%d\n",query_path(u,v,id,true));
}
}
printf("%d",1/0);//Don't copy my code !
}
int main(){
build();
solve();
return 0;
}
T3:(正解)
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int maxn=100005;
int next[maxn*5],hash[maxn*5],first[maxn],last[maxn],np_;
void add_file(int u,int num){//連結串列實現按位置儲存檔案
hash[++np_]=num;
if(first[u]) next[last[u]]=np_;
else first[u]=np_;
last[u]=np_;
}
int hash_num(char* s){
int ret=0;
for(int i=0;s[i]!=0;i++) ret=ret*27+s[i]-96;
return ret;
}
struct edge{//儲存樹邊
int to,next;
}e[maxn*2];
int head[maxn],np;
void adde(int u,int v){//新增樹邊
e[++np]=(edge){v,head[u]};
head[u]=np;
e[++np]=(edge){u,head[v]};
head[v]=np;
}
struct ext_name{
int v,id;
const bool operator < (const ext_name& B)const {
return v<B.v;
}
}D[maxn*6];
bool cmp(const ext_name& A,const ext_name& B){
return A.v<B.v||(A.v==B.v&&A.id<B.id);
}
int n,m,k[maxn];
/*--------------------線段樹----------------------*/
int rt,nowpos,lc[maxn*12],rc[maxn*12],sum[maxn*12],setv[maxn*12];
#define pushup(now) sum[now]=sum[lc[now]]+sum[rc[now]]
void pushdown(int now,int l,int r,int M){
if(setv[now]!=-1) return;
sum[lc[now]]=sum[rc[now]]=0;
setv[lc[now]]=setv[rc[now]]=-1;
setv[now]=0;
}
void build(int& now,int l,int r){
now=++nowpos;
if(l==r) {sum[now]=1;return;}
int M=(l+r)>>1;
build(lc[now],l,M);
build(rc[now],M+1,r);
pushup(now);
}
void update(int now,int l,int r,int x,int y){
if(x<=l&&r<=y) {setv[now]=-1,sum[now]=0;return;}
int M=(l+r)>>1;
pushdown(now,l,r,M);
if(y<=M) update(lc[now],l,M,x,y);
else if(x>M) update(rc[now],M+1,r,x,y);
else update(lc[now],l,M,x,y),update(rc[now],M+1,r,x,y);
pushup(now);
}
int query(int now,int l,int r,int x,int y){
if(x<=l&&r<=y) return sum[now];
int M=(l+r)>>1;
pushdown(now,l,r,M);
if(y<=M) return query(lc[now],l,M,x,y);
else if(x>M) return query(rc[now],M+1,r,x,y);
else return query(lc[now],l,M,x,y)+query(rc[now],M+1,r,x,y);
}
/*-------------------樹鏈剖分--------------------*/
int fa[maxn],son[maxn],dep[maxn],sz[maxn];
int P,top[maxn],seg[maxn],rev[maxn],low[maxn];
int lP[maxn],tP[maxn],pos;
void dfs1(int i,int Fa){
dep[i]=dep[Fa]+1,fa[i]=Fa,sz[i]=1;
for(int p=head[i];p;p=e[p].next){
int j=e[p].to;
if(j==Fa) continue;
dfs1(j,i);
sz[i]+=sz[j];
if(sz[son[i]]<sz[j]) son[i]=j;
}
}
void load_files(int segl,int segr){
sort(D+lP[segl],D+tP[segr]+1,cmp);
}
void init_folder(int u){
int i=rev[u];
lP[u]=pos+1;
if(k[i]){
for(int p=first[i];p;p=next[p])
D[++pos].v=hash[p],D[pos].id=u;
tP[u]=pos;
}
else D[++pos].v=-1,D[pos].id=u,tP[u]=pos;//如果為空資料夾裝載假檔案
}
void dfs2(int i){
if(sz[i]==1)
load_files(seg[top[i]],seg[i]),low[i]=i;//將檔案排序
if(son[i]){
top[son[i]]=top[i];
seg[son[i]]=++P,rev[P]=son[i];
init_folder(P);//將資料夾中的檔案寫入序列
dfs2(son[i]);
low[i]=low[son[i]];
}
for(int p=head[i];p;p=e[p].next){
int j=e[p].to;
if(top[j]) continue;
top[j]=j,seg[j]=++P,rev[P]=j;
init_folder(P);
dfs2(j);
}
}
int lca(int x,int y){
int fx=top[x],fy=top[y];
while(fx!=fy){
if(dep[fx]<dep[fy]) swap(fx,fy),swap(x,y);
x=fa[fx],fx=top[x];
}
if(dep[x]<dep[y]) return x;
return y;
}
int calc(int l,int r,int L__,int R__,int id,bool del){//最難懂的部分之一,用二分尋找子序列位置
ext_name Tmp;Tmp.v=id;
L__=lP[L__],R__=tP[R__];
int L_=lower_bound(D+L__,D+R__+1,Tmp)-D,L;
int R_=upper_bound(D+L__,D+R__+1,Tmp)-D,R;
if(L_>=R_) return 0;//沒有找到
R_--;
int l_=R_+1,r_=L_-1;
L=L_,R=R_;
while(L<=R){//二分尋找
int M=(L+R)>>1;
if(D[M].id>=l) l_=M,R=M-1;
else L=M+1;
}
L=L_,R=R_;
while(L<=R){
int M=(L+R)>>1;
if(D[M].id<=r) r_=M,L=M+1;
else R=M-1;
}
if(l_>r_) return 0;
int TMP=query(rt,1,pos,l_,r_);//線上段樹中查詢
if(del) update(rt,1,pos,l_,r_);//刪除檔案
return TMP;
}
int query_path(int x,int y,int id,bool Access_y){//樹鏈剖分中跳路徑的方法
int fx=top[x],fy=top[y],ret=0;
while(fx!=fy){
ret+=calc(seg[fx],seg[x],seg[fx],seg[low[fx]],id,false);
x=fa[fx],fx=top[x];
}
if(Access_y) ret+=calc(seg[y],seg[x],seg[top[y]],seg[low[y]],id,false);
else ret+=calc(seg[y]+1,seg[x],seg[top[y]],seg[low[y]],id,false);
return ret;
}
int del_path(int x,int y,int id){
int fx=top[x],fy=top[y],ret=0;
while(fx!=fy){
ret+=calc(seg[fx],seg[x],seg[fx],seg[low[fx]],id,true);
x=fa[fx],fx=top[x];
}
ret+=calc(seg[y],seg[x],seg[top[y]],seg[low[y]],id,true);
return ret;
}
void build(){
int u,v;
char ext_nm[10];
scanf("%d%d",&n,&m);
for(int i=1;i<n;i++) scanf("%d%d",&u,&v),adde(u,v);
for(int i=1;i<=n;i++){
scanf("%d",k+i);
for(int j=1;j<=k[i];j++){
scanf("%s",ext_nm);
add_file(i,hash_num(ext_nm));//插入到連結串列
}
}
dfs1(1,0);
seg[1]=top[1]=P=rev[1]=1;
init_folder(1);
dfs2(1);
build(rt,1,pos);//初始化線段樹
}
void solve(){
char cmd[3][10];
int u,v,a,id;
while(m--){
scanf("%s%s",cmd[0],cmd[1]);
if(cmd[0][0]=='q'){
if(cmd[1][1]=='p'){
scanf("%d%d",&u,&v);
printf("%d\n",dep[u]+dep[v]-2*dep[lca(u,v)]);
}
else{
scanf("%d%d%s",&u,&v,cmd[2]);
a=lca(u,v),id=hash_num(cmd[2]+2);
printf("%d\n",query_path(u,a,id,true)+query_path(v,a,id,false));
}
}
else{
scanf("%d%d%s",&u,&v,cmd[2]);
a=lca(u,v),id=hash_num(cmd[2]+2);
printf("%d\n",del_path(u,a,id)+del_path(v,a,id));
}
}
printf("%d",1/0);//Dont't copy my code !
}
int main(){
build();
solve();
return 0;
}