NOI2018 简要题解

command_block

2021-06-15 09:34:10

Personal

$$\large\textbf{Task Clear}$$ $$\large\text{Score : 515}$$ # D1T1 [归程](https://www.luogu.com.cn/problem/P4768) **题意** : 给出一张 $n$ 个点 $m$ 条边的无向图,边有(长度,海拔)两个参数。 有 $q$ 个询问 : 从 $u$ 点出发前往 $1$ ,初始时可以乘车,但车不能经过海拔 $\leq q$ 的边。 可以在合适的时机下车行走,最小化行走的路程。 $n\leq 2\times 10^5,m,q\leq 4\times 10^5$ ------------ 签到题。 先从 $1$ 号点跑一个单源最短路,若在点 $v$ 下车,则行走路程为 $dis_v$。 以“海拔”建立克鲁斯卡尔重构树,则“点 $u$ 经过 $>q$ 的边能到达的区域”可以被描述为重构树的一个子树。 子树取 $\min$ 即可得到答案,复杂度 $1\log$。 ```cpp #include<algorithm> #include<cstdio> #include<queue> #define Pr pair<int,int> #define fir first #define sec second #define mp make_pair #define pb push_back #define MaxN 200500 #define MaxM 400500 using namespace std; int INF=2000000000; int n,dis[MaxN<<1]; vector<int> g[MaxN<<1],l[MaxN]; priority_queue<Pr> q; void dijk(int s) { for (int i=1;i<=n;i++)dis[i]=INF; dis[s]=0;q.push(mp(0,s)); while(!q.empty()){ Pr now=q.top();q.pop(); if (dis[now.sec]!=-now.fir)continue; int u=now.sec; for (int i=0,v;i<g[u].size();i++) if (dis[v=g[u][i]]>dis[u]+l[u][i]){ dis[v]=dis[u]+l[u][i]; q.push(mp(-dis[v],v)); } } } int w[MaxN<<1],f[18][MaxN<<1]; int qry(int u,int lim) { int k=17; while(k--) if (w[f[k][u]]>lim) u=f[k][u]; return dis[u]; } int fa[MaxN<<1]; int find(int u) {return fa[u]==u ? u : fa[u]=find(fa[u]);} bool chk(int u,int v) {return find(u)!=find(v);} void merge(int u,int v) {fa[find(u)]=find(v);} struct Line{int u,v,w;}s[MaxM]; bool cmp(const Line &A,const Line &B) {return A.w>B.w;} int m,Q,K,S; void solve() { scanf("%d%d",&n,&m); for (int i=1;i<=n;i++){g[i].clear();l[i].clear();} for (int i=1,u,v,len,a;i<=m;i++){ scanf("%d%d%d%d",&u,&v,&len,&a); g[u].pb(v);l[u].pb(len); g[v].pb(u);l[v].pb(len); s[i]=(Line){u,v,a}; }dijk(1); for (int i=1;i<=n;i++)g[i].clear(); for (int i=1;i<=n+n;i++)fa[i]=i; sort(s+1,s+m+1,cmp); int tn=n; for (int i=1;i<=m;i++){ int u=s[i].u,v=s[i].v; if (chk(u,v)){ u=fa[u];v=fa[v]; w[f[0][u]=f[0][v]=++tn]=s[i].w; g[tn].pb(u);g[tn].pb(v); dis[tn]=min(dis[u],dis[v]); merge(u,tn);merge(v,tn); } } for (int j=1;j<17;j++) for (int i=1;i<=tn;i++) f[j][i]=f[j-1][f[j-1][i]]; scanf("%d%d%d",&Q,&K,&S); for (int i=1,u,lim,las=0;i<=Q;i++){ scanf("%d%d",&u,&lim); u=(u+K*las-1)%n+1; lim=(lim+K*las)%(S+1); printf("%d\n",las=qry(u,lim)); } } int main() { int T;scanf("%d",&T); while(T--)solve(); return 0; } ``` # D1T2 [冒泡排序](https://www.luogu.com.cn/problem/P4769) **题意** : 对于排列,记数值 $i$ 原来的位置为 $p_i$ ,则相邻交换排序的交换次数有显然的下界 $\dfrac{1}{2}\sum\limits_{i=1}^n\big|i-p_i\big|$。 对于一个排列,若对其冒泡排序所需的交换次数达到上述下界,则称为好的。 求长度为 $n$ ,且字典序严格大于给定的排列 $p$ 的好排列总数,答案对 $998244353$ 取模。 多组数据, $T\leq 5,n\leq 6\times 10^5,\sum n\leq 2\times10^6$ ------------ 牛逼题。 - ### **Part1** 考虑好排列的充要条件。 我们知道冒泡排序所需的交换次数达到下确界(每一步交换都是必须的),于是我们可以构造任意的交换方案。 每次交换我们都应当使 $\sum\limits_{i=1}^n\big|i-p_i\big|$ 减少 $2$ (也至多减少 $2$),这样才可能符合条件。 若 $p_i\leq i$ ,则数值 $i$ 只能向右移动(不能走弯路),若 $i<p_i$ ,则数值 $i$ 只能向左移动。(充要) 因此,若 $p_i\geq i$ ,则 $p_i$ 左侧的数都应该比 $i$ 小。若 $p_i>i$ ,则 $p_i$ 右侧的数都应该比 $i$ 大。(充要) 进一步观察发现,这等价于要求不存在长度为 $3$ 的下降子序列(三连降)。 - **必要性** : 若存在三连降,考虑中间的数 $b$,无论 $p_b\leq b$ 还是 $p_b\geq b$ ,由于两侧都有不对的数,都不符合条件。 - **充分性** : 若 $p_i\leq i$ ,且我们要交换 $p_i,p_{i+1}$ ,此时 $p_i,p_{i+1}$ 已经构成长度为 $2$ 的下降子序列,于是 $p_i$ 左侧不可能有 $>i$ 的数。 类似地, $p_i\geq i$ 时也能保证正确性。 显然交换不会中途产生三连降。 - ### **Part2** 先不考虑字典序的限制。 用 $\rm DP$ 来求解长为 $n$ 的好排列的个数。 设 $f[i,j]$ 表示填写了前 $i$ 个数,其中最大值为 $j$ 的方案数。 下一步,若填更大的值,则必然合法。若填 $<j$ 的值,则必须填最小值,否则会产生三连降。 于是有转移 : $$f[i,j]=\sum\limits_{k=1}^jf[i-1,k]$$ 利用差分,能发现 $f[i,j]=f[i,j-1]+f[i-1,j]$。 尝试编一个组合意义 : 路径。 - 当位于 $(i,j)$ 时,可以前往 $(i+1,j)$ 或 $(i,j+1)$。但不能使得 $i>j$。 先不考虑「不能使得 $i>j$」的限制,从 $(0,0)$ 到达 $(n,k)$ 的方案数显然为 $\dbinom{n+k}{n}$。 「使得 $i>j$ 」等价于「达到直线 $i=j+1$」。我们将不合法方案第一次触碰 $i=j+1$ 之后的路径根据 $i=j+1$ 翻转。 于是,不合法方案与「从 $(0,0)$ 到 $(k-1,n+1)$ 的路径」一一对应。 综上我们能得到 : $$f[n,k]=\dbinom{n+k}{n}-\dbinom{n+k}{n+1}$$ - ### **Part3** 接下来我们考虑字典序的限制。 枚举第一次不卡下界的位置,可以转化为 : 钦定一个前缀 $=A_{1\sim k-1}$,且下一个数 $>A_k$ 的所有排列中,好排列的个数。 首先判一下填写这个数是否已经未被了上述 $\rm DP$ 转移的限制。 记 $L=\max A_{1\sim k-1}$ , $t$ 是不在 $A_{1\sim k-1}$ 中的最小的数。 注意到总有 $t\leq A_k$ ,于是我们在这一位总是不能选择填 $t$。 于是只能填比 $\max(A_k,L)$ 大的数。 相当于将 $f[i,\max(A_k,L)+1\sim \infty]$ 置为 $1$ ,然后 $\rm DP$ 求出 $f[n,n]$ ,即为答案。 转化为下列问题 :将 $f[i,j\sim \infty]$ 置为 $1$ ,然后 $\rm DP$ 求出 $f[n,n]$ 。 考虑 $f[i,j]\rightarrow f[n,m]$ 的贡献系数,相当于从 $(i,j)$ 走到 $n,m$ 且不使 $i>j$ 的方案数。 利用类似的技巧不难算得 : $${\rm contrib}\big(f[i,j]\rightarrow f[n,n]\big)=\dbinom{2n-i-j}{n-i,n-j}-\dbinom{2n-i-j}{n+1-i,n-1-j}$$ 我们要求的是 $\sum_{j=k}^{\infty}{\rm contrib}\big(f[i,j]\rightarrow f[n,n]\big)$ ,不难发现其等于 ${\rm contrib}\big(f[i-1,j]\rightarrow f[n,n]\big)$ 复杂度 $O(n)$。 ```cpp #include<algorithm> #include<cstdio> #define ll long long #define MaxN 600500 using namespace std; const int mod=998244353; ll powM(ll a,int t=mod-2){ ll ret=1; while(t){ if (t&1)ret=ret*a%mod; a=a*a%mod;t>>=1; }return ret; } int fac[MaxN<<1],ifac[MaxN<<1]; int C(int n,int m) {return m<0||n<m ? 0 : 1ll*fac[n]*ifac[m]%mod*ifac[n-m]%mod;} void Init(int n) { fac[0]=1; for (int i=1;i<=n;i++) fac[i]=1ll*i*fac[i-1]%mod; ifac[n]=powM(fac[n]); for (int i=n;i;i--) ifac[i-1]=1ll*i*ifac[i]%mod; } int n;bool vis[MaxN]; int s(int i,int j) {return (C(n+n-i-j,n-i)-C(n+n-i-j,n+1-i)+mod)%mod;} void solve() { scanf("%d",&n); for (int i=1;i<=n+1;i++)vis[i]=0; int ans=0;bool fl=0; for (int i=1,mx=0,mn=1,p;i<=n;i++){ scanf("%d",&p);mx=max(mx,p); vis[p]=1;while(vis[mn])mn++; if (!fl)ans=(ans+s(i-1,mx+1))%mod; if (mn<p&&p<mx)fl=1; }printf("%d\n",ans); } int main() { Init(1200000); int T;scanf("%d",&T); while(T--)solve(); return 0; } ``` # D1T3 [你的名字](https://www.luogu.com.cn/problem/P4770) **题意** : 给出文本串 $S$。 $q$ 次询问,每次给出串 $T$ 与区间 $[l,r]$ ,求 $T$ 的不在 $S[l,r]$ 中出现过的本质不同子串数目。 $|S|\leq 5\times10^5,q\leq 10^5,\sum|T|\leq 10^6$ ------------ 套路题。 首先考虑 $l=1,r=|S|$ 怎么做。 记 $slen[i]$ 表示 $T[1,i]$ 在 $S$ 中匹配的最长后缀长度。 这是一个经典问题,可以使用双指针求解 : - 类似 AC 自动机,要添加字符时,若当前节点无出边,则跳父亲,直到有出边为止。 求出 $slen$ 数组后,对 $T$ 建立 SAM。 对于某个 SAM 节点,记串长区间为 $(L,R]$ ,endpos 集合为 $E$ ,则找出任意一个 $d=\in E$ ,该节点中不在 $S$ 中出现的串的个数为 $\max\big(0,R-max(L,slen[d])\big)$。 接下来考虑 $l,r$ ,仍然考虑求出 $slen$ 数组。 观察我们在双指针算法中做了什么 : - 查看是否存在出边 - 跳父亲 在 $S$ 的 SAM 上用线段树合并维护 endpos 。 查看字符 $c$ 的出边时,记当前的匹配长度为 $len$ ,当前节点为 $u$。 找出 $v=u.trans(c)$ ,查看 ${\rm edp}(v)$ 中是否存在 $d\in[l+len,r]$。 若满足条件,则 $u\leftarrow v$ ,且 $len$ 加一。 若 $v$ 不存在或不符合条件,则 $len$ 减一,若 $len$ 不在 $u$ 的串长区间内则跳父亲。 复杂度 $O\Big(\big(|S|\sum|T|\big)\log |S|\Big)$ ```cpp #include<algorithm> #include<cstring> #include<cstdio> #include<vector> #define pb push_back #define MaxN 500500 using namespace std; struct SAM_Node{int t[26],f,len;}a[MaxN<<1],b[MaxN<<1]; int tn,las; void ins(int c,SAM_Node *a) { int p=las,np=++tn;las=np; a[np].len=a[p].len+1; for (;p&&!a[p].t[c];p=a[p].f)a[p].t[c]=np; if (!p)a[np].f=1; else { int v=a[p].t[c]; if (a[v].len==a[p].len+1)a[np].f=v; else { int nv=++tn;a[nv]=a[v]; a[nv].len=a[p].len+1; for (;p&&a[p].t[c]==v;p=a[p].f)a[p].t[c]=nv; a[np].f=a[v].f=nv; } } } struct Node{int l,r;}t[MaxN*42]; int tot,to,rt[MaxN<<1]; void add(int l,int r,int &u) { if (!u)u=++tot; if (l==r)return ; int mid=(l+r)>>1; if (to<=mid)add(l,mid,t[u].l); else add(mid+1,r,t[u].r); } int wfl,wfr; bool qry(int l,int r,int u) { if (!u)return 0; if (wfl<=l&&r<=wfr)return 1; int mid=(l+r)>>1; if (wfl<=mid&&qry(l,mid,t[u].l))return 1; if (mid<wfr&&qry(mid+1,r,t[u].r))return 1; return 0; } int merge(int u,int v) { if (!u||!v)return u|v; int p=++tot; t[p].l=merge(t[u].l,t[v].l); t[p].r=merge(t[u].r,t[v].r); return p; } vector<int> g[MaxN<<1]; void dfs(int u) { for (int i=0;i<g[u].size();i++){ dfs(g[u][i]); rt[u]=merge(rt[u],rt[g[u][i]]); } } int ed[MaxN<<1]; void dfs2(int u) { for (int i=0;i<g[u].size();i++){ dfs2(g[u][i]); ed[u]=ed[g[u][i]]; } } int n; bool chk(int u,int len,int l,int r,char c){ wfl=l+len;wfr=r; return wfl<=wfr&&a[u].t[c]&&qry(1,n,rt[a[u].t[c]]); } char s[MaxN]; int m,q,slen[MaxN]; int main() { scanf("%s",s+1); n=strlen(s+1); tn=las=1; for (int i=1;i<=n;i++)ins(s[i]-'a',a); for (int i=2;i<=tn;i++)g[a[i].f].pb(i); for (int i=1,p=1;i<=n;i++){ p=a[p].t[s[i]-'a']; to=i;add(1,n,rt[p]); }dfs(1); for (int i=1;i<=tn;i++)g[i].clear(); scanf("%d",&q); for (int qt=1,l,r;qt<=q;qt++){ scanf("%s%d%d",s+1,&l,&r); m=strlen(s+1); for (int i=1,p=1,len=0;i<=m;i++){ while(len){ if (chk(p,len,l,r,s[i]-'a'))break; len--; if (len==a[a[p].f].len)p=a[p].f; } if (chk(p,len,l,r,s[i]-'a')){ p=a[p].t[s[i]-'a']; len++; }slen[i]=len; } tn=las=1; for (int i=1;i<=m;i++)ins(s[i]-'a',b); for (int i=1,p=1;i<=m;i++) ed[p=b[p].t[s[i]-'a']]=i; for (int i=2;i<=tn;i++)g[b[i].f].pb(i); dfs2(1); long long ans=0; for (int u=2;u<=tn;u++) ans+=max(0,b[u].len-max(b[b[u].f].len,slen[ed[u]])); for (int i=1;i<=tn;i++)g[i].clear(); memset(b,0,sizeof(SAM_Node)*(tn+5)); printf("%lld\n",ans); }return 0; } ``` # D2T1 [屠龙勇士](https://www.luogu.com.cn/problem/P4774) **题意** : 玩家需要按照 $1\sim n$ 的顺序逐次开 $n$ 把锁。每把锁有两个权值 $(p_i,a_i)$。 在开锁之前,要事先选定参数 $x$ ,之后不可更改。 初始时玩家手上有 $m$ 把钥匙,每个钥匙有一个正整数权值。 开锁 $(p,a)$ 时,找出当前权值 $\leq p$ 且最大的钥匙 $k$,若 $kx\geq a$ 且 $kx\bmod p=a$ ,则开锁成功。 开锁成功后,使用过的钥匙会消失,玩家会获得一把新的钥匙(权值确定)。 求 $x$ 的最小解,或指出无解。 $n,m\leq 10^5$ ,全体 $p_i$ 的最小公倍数 $\leq 10^{12}$。 ------------ (签到)科技题。 首先,若有解,每一步使用的钥匙是可以确定的。可以用 `multiset` 求出。 然后就变成了如下问题 : 给定 $n$ 条形如 $k_ix=a\pmod {p_i}$ 的方程与 $k_ix\geq a_i$ 的不等式,求最小整数解。 对于不等式,先求出 $x$ 的下界,求出同余方程组通解后不难得到最小解。 问题转化为解同余方程组。 考虑将 $kx=a\pmod p$ 变形为 $x=c\pmod p$ 的经典形式。 先特判 $0$ : 当 $k=a=0\pmod p$ 时方程恒成立,不考虑。当 $k=0\pmod p$ 但$c\neq 0\pmod p$时无解。 接下来,由于 $k,p$ 不一定互质,我们不能直接通过逆元来转化。 不过,我们仍然能尝试求解不定方程 $kx+py=c$ ,无解则原方程组无解。 否则由斐蜀定理得有解的充要条件 $\gcd(k,p)|c$ ,设 $d=\gcd(k,p)$。 则有 $\dfrac{k}{d}x+\dfrac{p}{d}y=\dfrac{c}{d}$ 。 我们对 $\dfrac{p}{d}$ 取模即得到 $\dfrac{k}{d}x=\dfrac{c}{d}\pmod {\dfrac{p}{d}}$ 由于$\dfrac{k}{d}\perp\dfrac{p}{d}$ ,我们就可以求逆元变为经典形式了。 剩下的就是一个扩展中国剩余定理。 复杂度 $O((n+m)\log)$。 ```cpp #include<algorithm> #include<cstdio> #include<set> #define Itor set<ll>::iterator #define ll long long #define MaxN 100500 using namespace std; const int mod=998244353; ll mul(ll a,ll b,ll m) {return (((a>>20)*b%m<<20)+(a-(a>>20<<20))*b)%m;} ll gcd(ll a,ll b) {return !b ? a : gcd(b,a%b);} ll lcm(ll a,ll b) {return a/gcd(a,b)*b;} void exgcd(ll a,ll b,ll &x,ll &y){ if (!b){x=1;y=0;return ;} exgcd(b,a%b,y,x);y-=(a/b)*x; } ll inv(ll a,ll m){ ll x,y;exgcd(a,m,x,y); return (x+m)%m; } bool fl; void merge(ll &c1,ll &p1,ll c2,ll p2) { ll c=(c2-c1%p2+p2)%p2,d=gcd(p1,p2); if (c%d){puts("-1");fl=1;return ;} ll k,y;exgcd(p1,p2,k,y); ll p=lcm(p1,p2); c1=(c1+mul(mul(k,p1,p),c/d,p))%p; p1=p; } multiset<ll> s; ll a[MaxN],k[MaxN],p[MaxN],t[MaxN] ,mx,tc,tp; void calc(ll k,ll c,ll p) { mx=max(mx,(c-1)/k+1); if (k%p==0){ if (c%p==0)return ; else {puts("-1");fl=1;return ;} } ll d=gcd(k,p); if (c%d){puts("-1");fl=1;return ;} k/=d;p/=d;c/=d; c=mul(c,inv(k,p),p); merge(tc,tp,c,p); } int n,m; void solve() { scanf("%d%d",&n,&m); for (int i=1;i<=n;i++)scanf("%lld",&a[i]); for (int i=1;i<=n;i++)scanf("%lld",&p[i]); for (int i=1;i<=n;i++)scanf("%lld",&t[i]); s.clear(); for (int i=1;i<=m;i++){ ll x;scanf("%lld",&x); s.insert(x); } for (int i=1;i<=n;i++){ Itor it=s.upper_bound(a[i]); if (it!=s.begin())it--; k[i]=*it;s.erase(it); s.insert(t[i]); } mx=0;tp=1;tc=0;fl=0; for (int i=1;i<=n&&!fl;i++) calc(k[i],a[i],p[i]); if (fl)return ; ll tk=mx/tp; while(tk*tp+tc<mx)tk++; printf("%lld\n",tk*tp+tc); } int main() { int T;scanf("%d",&T); while(T--)solve(); return 0; } ``` # D2T2 [情报中心](https://www.luogu.com.cn/problem/P4775) 见 [题解 【P4775 [NOI2018] 情报中心】](https://www.luogu.com.cn/blog/command-block/solution-p4775) # D2T3 [多边形](https://www.luogu.com.cn/problem/P4776) **题意** : 给出一棵 $n$ 个节点的有根树。 dfs 该树,优先走编号小的儿子,按照访问顺序将叶子排成一个序列 $A$。 对于叶子 $u,v$ ,若他们在 $A$ 中的位置为 $i,j$ ,记 $d(u,v)=\min\big(|i-j|,|A|-|i-j|\big)$ 建立新的无向图 $G$ 。$u,v$ 之间有边当且仅当 : - $d(u,v)\leq K$。 - $u,v$ 在树中直接相连。 求图 $G$ 的哈密尔顿回路数目,答案对 $998244353$ 取模。 $n\leq 1000,k\leq 3$。 ------------ 丧心病狂的大胖题…… 要不起。 $15$ 分暴力,走了。 ```cpp #include<algorithm> #include<cstdio> #include<vector> #define pb push_back #define MaxN 1050 using namespace std; vector<int> t[MaxN],g[MaxN]; int p[MaxN],tn; void pfs(int u) { if (!t[u].size())p[++tn]=u; sort(t[u].begin(),t[u].end()); for (int i=0;i<t[u].size();i++) pfs(t[u][i]); } bool vis[MaxN];int n,k,ans; void dfs(int u,int cnt) { if (cnt==n){ for (int i=0;i<g[u].size();i++) if (g[u][i]==1)ans++; return ; } vis[u]=1; for (int i=0,v;i<g[u].size();i++) if (!vis[v=g[u][i]])dfs(v,cnt+1); vis[u]=0; } int main() { scanf("%d%d",&n,&k); if (n>20)return 0; for (int i=2,f;i<=n;i++){ scanf("%d",&f); t[f].pb(i); g[f].pb(i);g[i].pb(f); }pfs(1); k=min(k,tn/2); for (int i=1;i<=tn;i++) for (int j=i+1;j<=i+k;j++){ int v=(j+tn-1)%tn+1; g[p[i]].pb(p[v]); g[p[v]].pb(p[i]); } dfs(1,1); printf("%d",ans/2); return 0; } ```