NOIP2018提高组Day2 解题报告

时间:2021-11-03 21:11:12

前言

关于\(NOIP2018\),详见此博客:NOIP2018学军中学游记(11.09~11.11)

\(Day2\)的题目和\(Day1\)比起来,真的是难了很多啊。

\(T1\):旅行(点此看题面

对于树的情况,显然可以把相邻的点全部存下来,排序一遍后依次遍历即可。

对于基环外向树的情况,一种简单的方法是每次断一条边,把它当成树的情况,这样是\(O(n^2)\)的。

但我考场上没想到这种做法,结果对于环上的情况单独讨论,结果把这题弄成了一个极为复杂的模拟题,总共打了两个小时才打完。不过我这样的做法貌似是\(O(n)\)的(如果不算\(sort\)的\(log\))。

具体是怎么做的就不讲了(比较麻烦),有兴趣可以看一下代码(要看的话最好画图理解一下)。

我个人还是比较推荐用删边的做法的。

\(P.S.\)貌似有一个加强版的题目:【洛谷5049】旅行(数据加强版),我这种做法是能过的。

代码如下:

#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\)时的答案,则我们要知道两个性质:

  1. 对于任意\(n,m\),满足\(ans_{n,m}=ans_{m,n}\)。(显然)
  2. 对于任意\(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}\)与\(ans_{i,i}\)之间是有一定数量关系的,然而我并没有找到规律。

但是没关系,我们还可以打表啊!

不难发现,对于每一条从左下向右上的斜线中,必然可以被分成两部分,其中前一部分全为\(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;
}