NOI2020 简要题解

command_block

2021-02-03 15:43:03

Personal

$$\large\text{Current Score : 400}$$ # D1T1 [美食家](https://www.luogu.com.cn/problem/P6772) **题意** : 有一张 $n$ 个点的有向图,第 $i$ 条边从 $v_i$ 连向 $u_i$ ,通过需要 $t_i$ 天。 玩家需要在时刻 $0$ 从点 $1$ 出发,经过 $T$ 个时刻恰好回到点 $1$。旅途中必须一直行走,不能在节点上停留。 每到达一次节点 $u$ ,就会得到 $w_u$ 的收益。 此外,还有 $m$ 次突发事件,形如 $(u,t,w)$ ,表示在时刻 $t$ 恰好在节点 $u$ 就能得到 $w$ 的额外收益。 求出收益的最大值。 $n\leq 50,t_i\leq 5,m\leq 500,T\leq 10^9$ ------------ - 子问题 : $t=1,m=0$ 此时所有边的耗时均为 $1$ ,且没有突发事件。 容易想到如下 $\rm DP$ : 设 $f[t][u]$ 为在时刻 $t$ 到达节点 $u$ 所能得到的最大收益。 则有转移 $f[t][u]=w[u]+\max\limits_{v\rightarrow u}f[t-1][v]$ 可以用 $\{\max,+\}$ 矩阵乘法来描述转移。具体地,构造转移矩阵 $A[v][u]=[v\rightarrow u]w[u]$ 若将 $f[t][\_]$ 看做一个向量法,则有 $f[t]=f[t-1]\times A$,即 $f[k]=f[0]\times A^k$。 由于矩阵乘法有结合律,使用矩阵快速幂即可加速转移。 - 原问题 现在我们要处理两个问题,一是边权,而是美食节。 对于一条长度为 $d$ 的边,可以将其拆成 $d$ 个点的一条链,其中点权均为 $0$。 但是,这样会使得总点数增大至 $wm$ 级别,无法承受。 可以让多条不同的出边共用同一条链,这样总点数就是 $nw$ 级别,可以承受。 有美食节的一轮中,转移是特殊的,不能直接使用一般的转移矩阵。 我们可以一段段加速转移,在遇到美食节时停止并处理特殊转移。 这就要求我们需要 $k$ 次将指定向量转移若干轮。 若每次都使用矩阵快速幂,复杂度为 $O(k(nw)^3\log T)$ ,无法通过。 注意到,对于矩阵 $A,B$ 和向量 $T$,有 $T\times (A\times B)=(T\times A)\times B$。 若按照前者计算,需要先计算矩阵乘法,但按照后者计算,就只需要计算向量和矩阵的乘法。 具体地,对于每个 $2$ 的幂预处理出转移矩阵,然后按位拆分后乘到向量中即可。 复杂度为 $O((nw^3)\log T+k(nw)^2\log T)$ ,别看数值复杂度达到了 $8\times 10^8$ ,其实可以轻松通过。 ```cpp #include<algorithm> #include<cstdio> #define ll long long #define MaxN 255 using namespace std; const ll INF=1ll<<60; int N; struct Matrix {ll a[MaxN][MaxN];}A,pw[35]; void times(Matrix &C,const Matrix &A,const Matrix &B) { for (int i=1;i<=N;i++) for (int j=1;j<=N;j++) C.a[i][j]=-INF; for (int i=1;i<=N;i++) for (int j=1;j<=N;j++) for (int k=1;k<=N;k++) C.a[i][k]=max(C.a[i][k],A.a[i][j]+B.a[j][k]); } void times(ll *C,const Matrix &A) { static ll S[MaxN]; for (int i=1;i<=N;i++) {S[i]=C[i];C[i]=-INF;} for (int i=1;i<=N;i++) for (int j=1;j<=N;j++) C[i]=max(C[i],S[j]+A.a[j][i]); } void trans(ll *C,int t) { for (int i=0;i<30;i++) if (t&(1<<i)) times(C,pw[i]); } struct Data{int tim,u,w;}b[MaxN]; bool cmp(const Data &A,const Data &B) {return A.tim<B.tim;} ll ans[MaxN]; int n,m,T,k,w[MaxN]; int main() { scanf("%d%d%d%d",&n,&m,&T,&k); for (int i=1;i<=n;i++)scanf("%d",&w[i]); N=n*5; for (int i=1;i<=N;i++) for (int j=1;j<=N;j++) A.a[i][j]=-INF; for (int u=1;u<=n;u++){ A.a[u][u+n]=0; A.a[u+n][u+2*n]=0; A.a[u+2*n][u+3*n]=0; A.a[u+3*n][u+4*n]=0; } for (int i=1,u,v,d;i<=m;i++){ scanf("%d%d%d",&u,&v,&d); A.a[u+(d-1)*n][v]=w[v]; } pw[0]=A; for (int i=1;i<30;i++) times(pw[i],pw[i-1],pw[i-1]); for (int i=1;i<=k;i++) scanf("%d%d%d",&b[i].tim,&b[i].u,&b[i].w); sort(b+1,b+k+1,cmp); for (int i=1;i<=N;i++)ans[i]=-INF; ans[1]=w[1]; for (int i=1;i<=k;i++){ trans(ans,b[i].tim-b[i-1].tim); ans[b[i].u]+=b[i].w; }trans(ans,T-b[k].tim); printf("%lld",ans[1]<0 ? -1 : ans[1]); return 0; } ``` # D1T2 [命运](https://www.luogu.com.cn/problem/P6773) **题意** : 给出一棵 $n$ 个点的树,以 $1$ 为根。 给出 $m$ 条树上路径 $(u,v)$ ,满足 $u$ 是 $v$ 的祖先。 给树的每条边染色(黑白),使得每条路径中至少有一条黑边,求方案数。答案对 $988244353$ 取模。 $n,m\leq 5\times 10^5$ ------------ 好题。 设 $f[u,k]$ 表示考虑一端在 $u$ 的子树中的所有路径,未满足的路径另一端深度最大值为 $k$ 的方案数。要求 $k<dep_u$。 若不存在未满足的路径,则记为 $f[u,0]$。 答案为 $f[1,0]$。 考虑所有从 $u$ 开始的路径 $(u,t)$ ,求出 $d=\max dep_t$。未考虑任何子树时,边界为 $f[u,d]=1$。 对于子树 $v$ ,有转移 : $$f'[u,k]=f[u,k]\sum\limits_{i\leq dep_u}f[v][i]+\sum\limits_{\max(k_1,k_2)=k}f[v,k_1]f[u,k_2]$$ 若边 $(u,v)$ 的权值为 $1$ ,则可以满足 $v$ 的所有路径,状态 $[v,k_1],[u,k_2]$ 合并之后会变成 $[u,k_2]$。 若边 $(u,v)$ 的权值为 $0$ ,状态 $[v,k_1],[u,k_2]$ 合并之后会变成 $[u,\max(k_1,k_2)]$。 可以进一步写成 : $$f'[u,k]=f[u,k]\sum\limits_{i\leq dep_u}f[v][i]+f[v,k]\sum\limits_{i\leq k}f[u,i]+f[u,k]\sum\limits_{i\leq k}f[v,i]-f[u,k]f[v,k]$$ 考虑优化,记 $g[u,k]=\sum\limits_{i\leq k}f[u,i]$ ,则转移可以写作 : $$f'[u,k]=f[u,k]\big(g[v,dep_u]+g[v,k]\big)+f[v,k]g[u,k]-f[u,k]f[v,k]$$ 注意到 $f$ 有值的位置不多,考虑使用线段树合并优化转移。 并不需要在线段树中维护 $g$ ,而只需要维护 $f$ 的区间和,然后一边合并一边计算。类似 [P5298 [PKUWC2018]Minimax](https://www.luogu.com.cn/problem/P5298)。 首先查询 $g[v,dep_u]$ ,记为一个固定常数 $C$。 若 $v$ 树为空,则打一个区间乘 $\big(C+g[v,k]\big)$ 的标记。 若 $u$ 树为空,则打一个区间乘 $g[u,k]$ 的标记。 具体实现见代码。 ```cpp #include<algorithm> #include<cstdio> #include<vector> #define pb push_back #define MaxN 500500 using namespace std; const int mod=998244353; vector<int> g[MaxN]; int dep[MaxN]; void pfs(int u) { for (int i=0,v;i<g[u].size();i++) if (!dep[v=g[u][i]]){ dep[v]=dep[u]+1; pfs(v); } } struct Data{ int l,r,s,tim; inline void ladd(int c) {s=1ll*s*c%mod;tim=1ll*tim*c%mod;} }a[MaxN*22]; int tn,rt[MaxN]; int cre(){a[++tn].tim=1;return tn;} inline void up(int u) {a[u].s=(a[a[u].l].s+a[a[u].r].s)%mod;} inline void ladd(int u) { if (a[u].tim!=1){ if (a[u].l)a[a[u].l].ladd(a[u].tim); if (a[u].r)a[a[u].r].ladd(a[u].tim); a[u].tim=1; } } int to; void add(int l,int r,int &u) { if (!u)u=cre(); if (l==r){a[u].s=(a[u].s+1)%mod;return ;} int mid=(l+r)>>1;ladd(u); if (to<=mid)add(l,mid,a[u].l); else add(mid+1,r,a[u].r); up(u); } int wfr,ret; void qry(int l,int r,int &u) { if (!u)return ; if (r<=wfr){ret=(ret+a[u].s)%mod;return ;} int mid=(l+r)>>1;ladd(u); qry(l,mid,a[u].l); if (mid<wfr)qry(mid+1,r,a[u].r); } int C; int merge(int u,int v,int su,int sv) { if (!u){a[v].ladd(su);return v;} if (!v){a[u].ladd(C+sv);return u;} if (a[u].l==a[u].r) a[u].s=(1ll*a[u].s*(C+sv)+1ll*a[v].s*(su+a[u].s))%mod; else { ladd(u);ladd(v); a[u].r=merge(a[u].r,a[v].r,(su+a[a[u].l].s)%mod,(sv+a[a[v].l].s)%mod); a[u].l=merge(a[u].l,a[v].l,su,sv); up(u); }return u; } int n,h[MaxN]; void dfs(int u) { to=h[u];add(0,n,rt[u]); for (int i=0,v;i<g[u].size();i++) if (dep[v=g[u][i]]>dep[u]){ dfs(v); wfr=dep[u];ret=0;qry(0,n,rt[v]);C=ret; rt[u]=merge(rt[u],rt[v],0,0); } } int m; int main() { scanf("%d",&n); for (int i=1,u,v;i<n;i++){ scanf("%d%d",&u,&v); g[u].pb(v);g[v].pb(u); }dep[1]=1;pfs(1); scanf("%d",&m); for (int i=1,u,v;i<=m;i++){ scanf("%d%d",&u,&v); if (dep[u]>dep[v])swap(u,v); h[v]=max(h[v],dep[u]); }dfs(1); ret=wfr=0;qry(0,n,rt[1]); printf("%d",ret); return 0; } ``` # D1T3 [时代的眼泪](https://www.luogu.com.cn/problem/P6774) **题意** : 区间定值域顺序对数。 $n\leq 10^5$ ------------ 见 [[DS记录]P6579 [Ynoi2019] Happy Sugar Life](https://www.luogu.com.cn/blog/command-block/ds-ji-lu-p6579-ynoi2019-happy-sugar-life) # D2T1 [制作菜品](https://www.luogu.com.cn/problem/P6775) **题意** : 本题是构造题。 有 $n$ 种原料,第 $i$ 种原料有 $d_i$ 克,所有原料共有 $m\times k$ 克。 玩家需要制作 $m$ 道菜,每道菜恰使用 $k$ 克原材料,且至多使用两种原材料。 构造一组做菜的方案,或指出无解。 $n\leq 500,m,k\leq 5000$ ,保证 $n-2\leq m$。 ------------ 观察数据范围,发现 $m$ 和 $n-1,n-2$ 的大小关系是解决本题的关键信息。 - **引理** $m\geq n-1$ 时必然有解。 考虑归纳,当 $n=1$ 时显然。 否则,考虑最少的原料,若数量 $\geq k$ ,则说明 $m\geq n$ ,用该原料做一道菜,做完后仍有 $m\geq n-1$。 若 $<k$ ,则配合最多的原料制作一道菜(一定能做出来), $m$ 减少 $1$ 的同时 $n$ 至少减少 $1$ ,仍满足 $m\geq n-1$。 对于 $m\geq n-1$ 的部分分,按照上述过程贪心构造即可。 对于 $m=n-2$ 的 情况,考虑一组解,在做同一个菜的两个原料之间连边,由于只有 $n-2$ 条边,所以最少有两个联通块。 不难想到将整个集合剖分成两个 $m=n-1$ 的情况。 这相当于要求 $(|S|-1)k=\sum\limits_{i\in S}d_i$ 即 $-k=\sum\limits_{i\in S}(d_i-k)$ `bitset` 优化可行性背包,复杂度为 $O\Big(\frac{n^2k}{w}\Big)$。 构造方案时,利用 DP 数组容易判定 选/不选 该物品是否可能可行。 ```cpp #include<algorithm> #include<cstdio> #include<bitset> #define MaxN 505 #define MaxK 10500 using namespace std; struct Data{int x,p;}s[MaxN],st[MaxN]; bool cmp(const Data &A,const Data &B) {return A.x<B.x;} int k; void constr(Data *a,int n,int m) { for (int i=1;i<=m;i++){ int p1=0,sav=m*k; for (int i=1;i<=n;i++) if (a[i].x>0&&a[i].x<sav) {sav=a[i].x;p1=i;} if (a[p1].x>=k){ printf("%d %d\n",a[p1].p,k); a[p1].x-=k;continue; } int p2=0;sav=0; for (int i=1;i<=n;i++) if (a[i].x>sav&&i!=p1) {sav=a[i].x;p2=i;} printf("%d %d %d %d\n",a[p1].p,a[p1].x,a[p2].p,k-a[p1].x); a[p2].x-=k-a[p1].x;a[p1].x=0; } } int n,m; bitset<MaxN*MaxK> e[MaxN]; bool fl[MaxN]; void solve() { scanf("%d%d%d",&n,&m,&k); for (int i=1;i<=n;i++) {scanf("%d",&s[i].x);s[i].p=i;} if (m>=n-1)constr(s,n,m); else { const int Zero=MaxN*MaxK/2; e[0][Zero]=1; for (int i=1;i<=n;i++){ int c=s[i].x-k; if (c>0)e[i]=e[i-1]|(e[i-1]<<c); else e[i]=e[i-1]|(e[i-1]>>(-c)); } if (!e[n][Zero-k]){puts("-1");return ;} else { for (int i=1;i<=n;i++)fl[i]=0; int now=Zero-k; for (int p=n;p;p--) if (!e[p-1][now]) {fl[p]=1;now-=s[p].x-k;} int tn=0; for (int i=1;i<=n;i++)if (fl[i])st[++tn]=s[i]; constr(st,tn,tn-1); tn=0; for (int i=1;i<=n;i++)if (!fl[i])st[++tn]=s[i]; constr(st,tn,tn-1); } } } int main() { int T;scanf("%d",&T); while(T--)solve(); return 0; } ``` # D2T2 [超现实树](https://www.luogu.com.cn/problem/P6776) 好题。 注意到 : 存在无穷多个无法长出的树 $\Leftrightarrow$ 存在深度无穷大的无法长出的树。 我们不妨设定一个很大的深度 $d$ ,然后考虑所有深度为 $d$ 的树 $S$,若存在某一棵无法长出,则树林是不完备的。 考虑化简集合 $S$ ,若树 $A$ 能生长出树 $B$ ,则无需判定树 $B$。 手玩不难发现,只需要考虑“链树” ,即除去叶子后为一条链的树。任意一棵树都能由等高的链树长出。 由此,若 $T$ 集合中存在非链树,则不可能长成掉任何一棵链树,这样的树可以抛弃。 现在 $T$ 中只剩下链树。对于链树中的某点 $u$ ,存在下列四种情况 : - ① 左侧为叶子或链,右侧为空 - ② 左侧为叶子或链,右侧为叶子 - ③ 右侧为叶子或链,左侧为空 - ④ 右侧为叶子或链,左侧为叶子 对于左右两侧都是叶子的点,则认为同时满足 ②④。 按照这个规则建立四叉树,合并后搜索。 子树 $u$ 是完备的,当且仅当下列条件之一成立 : - 某棵链树中 $u$ 是叶子 - $u$ 的各个子树都完备 复杂度 $O(n)$。 ```cpp ``` # D2T3 [翻修道路](https://www.luogu.com.cn/problem/P6777) 咕。