NOIP2018提高組Day2 解題報告
前言
關於\(NOIP2018\),詳見此部落格:NOIP2018學軍中學遊記(11.09~11.11)。
\(Day2\)的題目和\(Day1\)比起來,真的是難了很多啊。
\(T1\):旅行(點此看題面)
對於樹的情況,顯然可以把相鄰的點全部存下來,排序一遍後依次遍歷即可。
對於基環外向樹的情況,一種簡單的方法是每次斷一條邊,把它當成樹的情況,這樣是\(O(n^2)\)的。
但我考場上沒想到這種做法,結果對於環上的情況單獨討論,結果把這題弄成了一個極為複雜的模擬題,總共打了兩個小時才打完。不過我這樣的做法貌似是\(O(n)\)的(如果不算\(sort\)的\(log\))。
具體是怎麼做的就不講了(比較麻煩),有興趣可以看一下程式碼(要看的話最好畫圖理解一下)。
我個人還是比較推薦用刪邊的做法的。
程式碼如下:
#include<bits/stdc++.h> #define N 5000 #define add(x,y) (e[++ee].nxt=lnk[x],e[lnk[x]=ee].to=y) #define swap(x,y) (x^=y^=x^=y) using namespace std; int n,m,ee,cur,lnk[N+5],vis[N+5],data[N+5]; struct edge { int to,nxt; }e[(N<<1)+5]; class Class_TreeSolver//對於樹的情況 { public: inline void Solve(int x=1,int lst=0) { register int i,H=cur+1,T; for(printf("%d ",x),i=lnk[x];i;i=e[i].nxt) e[i].to^lst&&(data[++cur]=e[i].to); for(T=cur,sort(data+H,data+T+1),i=H;i<=T;++i) Solve(data[i],x); } }TreeSolver; inline void Begin_Circle(int,int); class Class_CircleTreeSolver//對於基環外向樹的情況 { private: int Top,used[N+5],StackH[N+5],StackT[N+5],StackPos[N+5]; class Class_CircleChecker//判斷一個節點是否在環中 { private: int Found,fa[N+5],In[N+5],vis[N+5],Depth[N+5]; public: Class_CircleChecker() {Found=0;} inline void Init(int x=1,int lst=0)//初始化 { register int i,y; for(vis[x]=1,i=lnk[x];i;i=e[i].nxt) { if(!(e[i].to^lst)) continue; if(vis[y=e[i].to]) { if(Depth[x]<Depth[y]) swap(x,y); while(Depth[x]>Depth[y]) In[x]=1,x=fa[x]; while(x^y) In[x]=In[y]=1,x=fa[x],y=fa[y]; return (void)(In[x]=Found=1); } if(Depth[e[i].to]=Depth[fa[e[i].to]=x]+1,Init(e[i].to,x),Found) return; } vis[x]=0; } inline bool InCircle(int x) {return In[x];}//判斷x是否在環中 }C; inline void dfs(int x,int lst)//普通的dfs { if(used[x]) return; if(C.InCircle(x)) return Begin_Circle(x,lst);//如果當前節點在環上,特殊處理 register int i,H=cur+1,T; for(used[x]=1,printf("%d ",x),i=lnk[x];i;i=e[i].nxt) !used[e[i].to]&&(data[++cur]=e[i].to); for(T=cur,sort(data+H,data+T+1),i=H;i<=T;++i) dfs(data[i],x); } inline void dfs_Circle(int x,int lst,int Begin,int OtherSide)//在環上dfs { if(used[x]) return;//如果訪問過當前節點,退出 register int i,H=cur+1,T; for(used[x]=1,printf("%d ",x),i=lnk[x];i;i=e[i].nxt) !used[e[i].to]&&(data[++cur]=e[i].to); for(T=cur,sort(data+H,data+T+1),i=H;i<T;++i)//注意這裡寫的是小於T { if(C.InCircle(data[i])) StackH[++Top]=i+1,StackT[Top]=T,StackPos[Top]=x,dfs_Circle(data[i],x,Begin,OtherSide),--Top;//如果依然是環上的節點,用棧存下當前節點資訊,繼續dfs else dfs(data[i],x);//否則換成普通的dfs } if(C.InCircle(data[T]))//如果最後一個元素在環上 { if(OtherSide&&data[StackH[Top]]<data[i]&&!used[OtherSide])//如果走這個節點沒有回到這個環的另一邊更優 { while(Top>1)//如果棧中元素個數大於1 { for(i=StackH[Top];i<=StackT[Top];++i) dfs(data[i],StackPos[Top]);//處理當前棧頂元素可到達的剩餘節點 --Top;//彈出 } while(StackH[1]<=StackT[1]&&data[StackH[1]]<OtherSide) dfs(data[StackH[1]++],Begin);//特殊處理棧中的最後一個元素 dfs_Circle(OtherSide,x,Begin,0);//搜尋另一端 } else dfs_Circle(data[i],x,Begin,OtherSide);//如果不是更優,依然走當前節點 } else dfs(data[i],x);//如果不在環上,就用普通的dfs } inline void Begin_Circle(int x,int lst)//開始處理環的情況 { register int i,H=cur+1,T,s1=0,s2=0; for(used[x]=1,printf("%d ",x),i=lnk[x];i;i=e[i].nxt) if(!used[e[i].to]&&C.InCircle(data[++cur]=e[i].to)) s1?s2=e[i].to:s1=e[i].to;//用s1和s2儲存當前點在環上的兩個鄰點 for(T=cur,sort(data+H,data+T+1),i=H;i<=T;++i) { if(C.InCircle(data[i])) StackH[++Top]=i+1,StackT[Top]=T,StackPos[Top]=x,dfs_Circle(data[i],x,x,data[i]^s1?s1:s2);//對於在環上的節點特殊處理 else dfs(data[i],x);//否則使用普通的dfs } } public: inline void Solve() {C.Init(),used[0]=1,dfs(1,0);} }CircleTreeSolver; int main() { register int i,j,k,x,y; for(scanf("%d%d",&n,&m),i=1;i<=m;++i) scanf("%d%d",&x,&y),add(x,y),add(y,x); if(m==n-1) TreeSolver.Solve();else CircleTreeSolver.Solve(); return 0; }
\(T2\):填數遊戲(點此看題面)
如果用\(ans_{x,y}\)表示\(n=x,m=y\)時的答案,則我們要知道兩個性質:
- 對於任意\(n,m\),滿足\(ans_{n,m}=ans_{m,n}\)。(顯然)
- 對於任意\(m>n+1\),滿足\(ans_{n,m}=ans_{n,n+1}*3^{m-n-1}\)。(我也不會證)
則不難發現,我們只需求出\(ans_{i,i}\)和\(ans_{i,i+1}(1\le i\le8)\),就可以推出全部答案。
據說\(ans_{i,i}\)與\(ans_{i-1,i-1}\)、\(ans_{i,i+1}\)
但是沒關係,我們還可以打表啊!
不難發現,對於每一條從左下向右上的斜線中,必然可以被分成兩部分,其中前一部分全為\(0\),後一部分全為\(1\)(或整條線全為\(0\)或\(1\))。
那麼,我們就可以列舉每條斜線中選擇多少個\(1\),然後對其進行\(O(nm(n+m))\)驗證,就可以在較快的時間內打完表了(實際上我打了一個下午)。
程式碼如下:
#include<bits/stdc++.h>
#define MOD 1000000007
#define N 8
#define swap(x,y) (x^=y^=x^=y)
#define GetID(x,y) ((x)-2<<1|(y))
using namespace std;
const int List[2*N]={12,36,112,336,912,2688,7136,21312,56768,170112,453504,1360128,3626752,10879488};//最後打出來的表(我將二維壓成了一維)
int n,m;
class Class_ListMaker//打表程式碼
{
private:
#define ListSize 8
int n,m,ans,num[N+5][N+5];
inline bool IsLegal(int x,int y)//驗證當前位置合法性
{
register int x1=x,y1=y+1,x2=x+1,y2=y;
while(x1+y1<=n+m)
{
if(num[x1][y1]^num[x2][y2]) return num[x1][y1]<num[x2][y2];
(x1^n?++x1:++y1),(y2^m?++y2:++x2);
}
return true;
}
inline bool check() {for(register int i=1,j;i<n;++i) for(j=1;j<m;++j) if(!IsLegal(i,j)) return false;return true;}//列舉每一個位置
inline void Solve(int s=2)//列舉所有情況
{
register int i;
if(s>n+m) return (void)(!((ans+=check())^MOD)&&(ans=0));
for(Solve(s+1),i=n;i;--i) s-i>=1&&s-i<=m&&(num[i][s-i]=1,Solve(s+1),0);
for(i=n;i;--i) s-i>=1&&s-i<=m&&(num[i][s-i]=0);
}
public:
inline void MakeList()
{
freopen("List.txt","w",stdout);
for(cout<<"const int List[2*N]={",n=2;n<=ListSize;++n) m=n,ans=0,Solve(),printf("%d,",ans),m=n+1,ans=0,Solve(),printf("%d%c",ans,n^ListSize?',':'}');//輸出表
putchar(';'),exit(0);
}
}M;
inline int quick_pow(int x,int y)
{
register int res=1;
while(y) (y&1)&&(res=1LL*res*x%MOD),x=1LL*x*x%MOD,y>>=1;
return res;
}
int main()
{
// M.MakeList();
freopen("game.in","r",stdin),freopen("game.out","w",stdout);
scanf("%d%d",&n,&m),n>m&&swap(n,m),printf("%d",n^1?(m<=n+1?List[GetID(n,m-n)]:1LL*List[GetID(n,1)]*quick_pow(3,m-n-1)%MOD):quick_pow(2,m));//依據規律輸出答案
return 0;
}
\(T3\):保衛王國(點此看題面)
這題的正解是動態DP,不會動態DP的可以先去做這道黑色的模板題。
好吧,實際上這題是可以用倍增+\(DP\)來解決的。
考慮用\(In_{x,0/1}\)來表示不選/選\(x\)節點時在\(x\)子樹內達成條件的最小代價(為方便起見,我們再用一個\(In_{x,2}\)來表示\(min(In_{x,0},In_{x,1})\)),並用\(Out_{x,0/1}\)來表示不選/選\(x\)節點時在\(x\)子樹外達成條件的最小代價。
這兩個陣列可以通過兩遍\(dfs\)來預處理。
然後,我們還要預處理出一個數組\(f_{x,y,sx(0/1),sy(0/1)}\)來表示當\(x\)不選/選,\(fa_{x,y}\)不選/選時,使屬於以\(fa_{x,y}\)為根子樹而不屬於以\(x\)為根子樹的所有節點達成條件的最小代價。
這個陣列可以在預處理\(fa\)陣列時一起預處理掉。
只要列舉\(fa_{i,j-1}\)選與不選就可以進行轉移了。
而對於詢問,我們就採用倍增\(LCA\)的思想向上跳。
如果兩個節點為祖先關係,則直接向上跳,一邊跳一邊用一個數組\(g_{0/1}\)記錄在當前節點不選與選時使當前子樹滿足條件分別需要的最小代價,最後答案就是\(g_{sx}+Out_{y,sy}\)。
而不為祖先關係的情況也是同理的,分別將兩個節點跳到\(LCA_{x,y}\)的左右子節點,然後列舉\(LCA_{x,y}\)選與不選,然後減去之前預處理時得到的答案,加上新求出的最小代價,即可求出答案。
具體實現見程式碼:
#include<bits/stdc++.h>
#define N 100000
#define add(x,y) (e[++ee].nxt=lnk[x],e[lnk[x]=ee].to=y)
#define swap(x,y) (x^=y^=x^=y)
#define min(x,y) ((x)<(y)?(x):(y))
#define Gmin(x,y) (x>(y)&&(x=(y)))
#define LL long long
#define INF 1e18
using namespace std;
int n,ee,lnk[N+5],Cost[N+5];
struct edge
{
int to,nxt;
}e[(N<<1)+5];
class Class_FIO
{
private:
#define Fsize 100000
#define tc() (A==B&&(B=(A=Fin)+fread(Fin,1,Fsize,stdin),A==B)?EOF:*A++)
#define pc(ch) (void)(FoutSize<Fsize?Fout[FoutSize++]=ch:(fwrite(Fout,1,Fsize,stdout),Fout[(FoutSize=0)++]=ch))
int Top,FoutSize;char ch,*A,*B,Fin[Fsize],Fout[Fsize],Stack[Fsize];
public:
Class_FIO() {A=B=Fin;}
inline void read(int &x) {x=0;while(!isdigit(ch=tc()));while(x=(x<<3)+(x<<1)+(ch&15),isdigit(ch=tc()));}
inline void readc(char &x) {while(isspace(x=tc()));}
inline void writeln(LL x) {if(!x) return pc('0'),pc('\n');x<0&&(pc('-'),x=-x);while(x) Stack[++Top]=x%10+48,x/=10;while(Top) pc(Stack[Top--]);pc('\n');}
inline void clear() {fwrite(Fout,1,FoutSize,stdout),FoutSize=0;}
}F;
class Class_TreeDP
{
private:
#define LogN 17
#define jump(x,y,p) (g_now[p][0]=min(g_lst[p][0]+f[x][y][0][0],g_lst[p][1]+f[x][y][1][0]),g_now[p][1]=min(g_lst[p][0]+f[x][y][0][1],g_lst[p][1]+f[x][y][1][1]),x=fa[x][y],g_lst[p][0]=g_now[p][0],g_lst[p][1]=g_now[p][1])
int Depth[N+5],fa[N+5][LogN+5];LL In[N+5][3],Out[N+5][2],f[N+5][LogN+5][2][2],g_now[2][2],g_lst[2][2];
inline void dfs1(int x)//第一遍dfs,預處理出Depth,In陣列
{
register int i;
for(i=lnk[x],In[x][0]=0,In[x][1]=Cost[x];i;i=e[i].nxt) e[i].to^fa[x][0]&&(Depth[e[i].to]=Depth[fa[e[i].to][0]=x]+1,dfs1(e[i].to),In[x][0]+=In[e[i].to][1],In[x][1]+=In[e[i].to][2]);
In[x][2]=min(In[x][0],In[x][1]);
}
inline void dfs2(int x)//第二遍dfs,預處理出Out,fa,f陣列
{
register int i;
f[x][0][0][0]=INF,f[x][0][1][0]=In[fa[x][0]][0]-In[x][1],f[x][0][0][1]=f[x][0][1][1]=In[fa[x][0]][1]-In[x][2];
for(i=1;i<=LogN;++i)
{
fa[x][i]=fa[fa[x][i-1]][i-1],
//列舉中間轉移點選與不選即可進行轉移
f[x][i][0][0]=min(f[x][i-1][0][0]+f[fa[x][i-1]][i-1][0][0],f[x][i-1][0][1]+f[fa[x][i-1]][i-1][1][0]),
f[x][i][0][1]=min(f[x][i-1][0][0]+f[fa[x][i-1]][i-1][0][1],f[x][i-1][0][1]+f[fa[x][i-1]][i-1][1][1]),
f[x][i][1][0]=min(f[x][i-1][1][0]+f[fa[x][i-1]][i-1][0][0],f[x][i-1][1][1]+f[fa[x][i-1]][i-1][1][0]),
f[x][i][1][1]=min(f[x][i-1][1][0]+f[fa[x][i-1]][i-1][0][1],f[x][i-1][1][1]+f[fa[x][i-1]][i-1][1][1]);
}
for(i=lnk[x];i;i=e[i].nxt) e[i].to^fa[x][0]&&(Out[e[i].to][0]=Out[e[i].to][1]=Out[x][1]+In[x][1]-In[e[i].to][2],Gmin(Out[e[i].to][1],Out[x][0]+In[x][0]-In[e[i].to][1]),dfs2(e[i].to),0);
}
public:
inline void Init() {dfs1(Depth[1]=1),dfs2(1);}
inline bool Identify(int x,int y) {return !(fa[x][0]^y&&fa[y][0]^x);}//判斷兩節點是否相鄰
inline LL GetAns(int x,int sx,int y,int sy)//採用倍增LCA的思想,倍增求解答案
{
register int i;
Depth[x]<Depth[y]&&(swap(x,y),swap(sx,sy)),g_lst[0][sx]=In[x][sx],g_lst[1][sy]=In[y][sy],g_lst[0][sx^1]=g_lst[1][sy^1]=INF;
for(i=0;Depth[x]^Depth[y];++i) if((Depth[x]^Depth[y])&(1<<i)) jump(x,i,0);
if(!(x^y)) return g_now[0][sy]+Out[y][sy];
for(i=LogN;~i;--i) if(fa[x][i]^fa[y][i]) jump(x,i,0),jump(y,i,1);
return min(In[fa[x][0]][0]+Out[fa[x][0]][0]-In[x][1]-In[y][1]+g_lst[0][1]+g_lst[1][1],In[fa[x][0]][1]+Out[fa[x][0]][1]-In[x][2]-In[y][2]+min(g_lst[0][0],g_lst[0][1])+min(g_lst[1][0],g_lst[1][1]));//返回答案
}
}T;
int main()
{
register int i,Q,x,y,sx,sy,TypeY;register char TypeX;
for(F.read(n),F.read(Q),F.readc(TypeX),F.read(TypeY),i=1;i<=n;++i) F.read(Cost[i]);
for(i=1;i<n;++i) F.read(x),F.read(y),add(x,y),add(y,x);
for(T.Init();Q;--Q) F.read(x),F.read(sx),F.read(y),F.read(sy),F.writeln(!sx&&!sy&&T.Identify(x,y)?-1:T.GetAns(x,sx,y,sy));
return F.clear(),0;
}