#树形dp,二次扫描换根法#JZOJ 3501 消息传递 with others
JZOJ 3501 消息传递
题目
有一藤蔓和一棵\(n(n\leq 2*10^5)\)个点的无根树,可以用1单位时间YC把藤蔓种在树的任意位置,
然后只要任意位置有藤蔓,它就会在一个单位时间内按照YC的意愿到相邻的一个位置延长,
YC想考考你最短什么时候藤蔓会覆盖整一棵树,由于YC出类拔萃,
他还想问你他把藤蔓种在哪里能使时间最短并输出全部符合题意的节点
分析
考虑树形dp,二次扫描换根法,假设以某个放置位置为根
先以1为根,设\(f[i]\)表示藤蔓从\(i\)被覆盖那一刻开始覆盖以i为根的子树所需要的最短时间,
那么\(f\)越大肯定越想早延长,所以按照子节点的\(f\)降序排序
那么\(f[i]=max\{f[son]+order_{son}\}\)
不换根可以做到\(n^2log_2n\)的时间复杂度以通过弱化版
换根就要考虑父节点对以子节点为根的树答案影响
如果这个点不是1,就可以不用考虑1的父节点对换根答案影响,否则把它加进刚刚的候选数组,因为现在要换根下传\(f\)
还是一样按照\(f\)大到小排序,然后用前缀\(max\)和后缀\(max\),然后用它更新子节点,顺便还能求答案(最大值就是答案)
然后在dp的时候这里的根\(f[x]\)表示的实际意义是\(x\)在以\(x\)的子节点为根的树的\(f\),然而1并没有父节点,所以就是刚刚提到不用加进去排序
然后\(suf\)为什么要减1就是因为原来的\(f[x]\)是不用计算答案的,所以后面整体前移一位
时间复杂度\(O(nlog_2n)\),至于为什么代码看得有点古怪,大概是\(\text{STL::vector}\)以0开头
代码
#include <cstdio>
#include <cctype>
#include <algorithm>
#include <vector>
#define rr register
using namespace std;
const int N=200011;
vector<int>pt[N];
int f[N],pre[N],suf[N],n,dp[N],ans;
inline signed iut(){
rr int ans=0; rr char c=getchar();
while (!isdigit(c)) c=getchar();
while (isdigit(c)) ans=(ans<<3)+(ans<<1)+(c^48),c=getchar();
return ans;
}
inline void print(int ans){
if (ans>9) print(ans/10);
putchar(ans%10+48);
}
bool cmp(int x,int y){return f[x]>f[y];}
inline void dfs1(int x){
for (rr int i=0;i<pt[x].size();++i) dfs1(pt[x][i]);
sort(pt[x].begin(),pt[x].end(),cmp);
for (rr int i=0;i<pt[x].size();++i) f[x]=max(f[x],f[pt[x][i]]+i+1);
}
inline void dfs2(int x){
if (x>1){
pt[x].push_back(x);
for (rr int i=pt[x].size()-2;~i;--i){
rr int t1=pt[x][i],t2=pt[x][i+1];
if (f[t1]<f[t2]) t1^=t2,t2^=t1,t1^=t2;
pt[x][i]=t1,pt[x][i+1]=t2;
}
}
pre[0]=suf[pt[x].size()+1]=0;
for (rr int i=1;i<=pt[x].size();++i) pre[i]=max(pre[i-1],f[pt[x][i-1]]+i);
for (rr int i=pt[x].size();i;--i) suf[i]=max(suf[i+1],f[pt[x][i-1]]+i);
dp[x]=pre[pt[x].size()],ans=min(ans,dp[x]);
for (rr int i=1;i<=pt[x].size();++i) f[pt[x][i-1]]=max(pre[i-1],suf[i+1]-1);
for (rr int i=0;i<pt[x].size();++i)
if (pt[x][i]!=x) dfs2(pt[x][i]);
}
signed main(){
n=iut();
for (rr int i=2;i<=n;++i)
pt[iut()].push_back(i);
ans=2e9,dfs1(1),dfs2(1);
print(ans+1),putchar(10);
for (rr int i=1;i<=n;++i)
if (ans==dp[i]) print(i),putchar(32);
return 0;
}
简单写就能跳过之题目
JZOJ 3500 物语
题目
一个\(n\)个点\(m-1\)条边的无向图,还有一条隐藏边,这条边只知道两个端点\(X,Y\)和\(Q\)次询问给出的长度\(W\),每次询问从点1到点\(n\)的最短路,如果到达不了就输出“+Inf”
分析
因为无向图,所以正反相同,从1跑一次单源最短路,记作\(dis[0][\_]\),再从\(n\)跑一次单源最短路,记作\(dis[1][\_]\)
那么答案为三种情况的其中1种。
1:不用经过这条边
2:1到\(x\),\(x\)到\(y\),\(y\)到\(n\)
3:1到\(y\),\(y\)到\(x\),\(x\)到\(n\)
然后跑\(\text{SPFA}\)被卡了,所以就写了\(\text{Dijkstra+Heap优化}\)
代码
#include <cstdio>
#include <cctype>
#include <queue>
#define rr register
using namespace std;
const int N=200011; typedef long long lll;
struct node{int y,w,next;}e[N<<3]; pair<lll,int>heap[N];
lll dis[2][N]; int n,k=1,Q,m,ls[N],cnt;
inline signed iut(){
rr int ans=0; rr char c=getchar();
while (!isdigit(c)) c=getchar();
while (isdigit(c)) ans=(ans<<3)+(ans<<1)+(c^48),c=getchar();
return ans;
}
inline void print(lll ans){
if (ans>9) print(ans/10);
putchar(ans%10+48);
}
inline void Push(pair<lll,int>w){
heap[++cnt]=w;
rr int x=cnt;
while (x>1){
if (heap[x>>1]>heap[x])
swap(heap[x>>1],heap[x]),x>>=1;
else return;
}
}
inline void Pop(){
heap[1]=heap[cnt--];
rr int x=1;
while ((x<<1)<=cnt){
rr int y=x<<1;
if (y<cnt&&heap[y+1]<heap[y]) ++y;
if (heap[y]<heap[x]) swap(heap[y],heap[x]),x=y;
else return;
}
}
inline void dijk(int sta,int poi){
for (rr int i=1;i<=n;++i) dis[poi][i]=1e16;
heap[++cnt]=make_pair(0,sta),dis[poi][sta]=0;
while (cnt){
rr lll t=heap[1].first; rr int x=heap[1].second;
Pop(); if (dis[poi][x]!=t) continue;
for (rr int i=ls[x];i;i=e[i].next)
if (dis[poi][e[i].y]>dis[poi][x]+e[i].w){
dis[poi][e[i].y]=dis[poi][x]+e[i].w;
Push(make_pair(dis[poi][e[i].y],e[i].y));
}
}
}
signed main(){
n=iut(); m=iut(); Q=iut();
for (rr int i=1;i<m;++i){
rr int x=iut(),y=iut(),w=iut();
e[++k]=(node){y,w,ls[x]},ls[x]=k;
e[++k]=(node){x,w,ls[y]},ls[y]=k;
}
rr int X=iut(),Y=iut();
dijk(1,0),dijk(n,1);
for (rr int i=1;i<=Q;++i,putchar(10)){
rr int W=iut(); rr lll now=dis[0][n];
now=min(now,dis[0][X]+W+dis[1][Y]);
now=min(now,dis[0][Y]+W+dis[1][X]);
if (now>=1e16) printf("+Inf");
else print(now);
}
return 0;
}
JZOJ 3757 洛谷 2354 [NOI 2014] 随机数产生器
题目
按照题意构造一个\(1\sim n*m\)的排列,然后把它依次填入一个二维数组,
接着从左上角走到右下角只能向右或向下走,通过这种方式可以得到一个长为\(n+m-1\)的序列,再将这个序列升序排序,问这个序列字典序最小的方案
分析
首先贪心,想到的是1肯定在第一个,接着1左下角(不包括正左边和正下方)、右上角(同理)都不能选,再选第二小,以此类推,
那我能用二维数据结构维护,但是256MB貌似会MLE
而且二维树状数组最值区间修改好像很难,那只能放弃这种想法,那得用时间换空间
那就暴力维护吧,我可以用\(Lmax,Rmin\)表示某一行左边的最右可选和右边的最左可选(因为选择区间肯定在中间且都能被选)
如果该数的位置满足,那么再用这个位置暴力更新\(Lmax,Rmin\),并输出这个数,时间复杂度\(O((n+m)n)\)
代码
#include <cstdio>
#include <cctype>
#define rr register
using namespace std;
const int N=5000;
int x0,A,B,C,D,n,m,ti,cnt,a[N*N],rk[N*N],lmax[N],rmin[N];
inline signed iut(){
rr int ans=0; rr char c=getchar();
while (!isdigit(c)) c=getchar();
while (isdigit(c)) ans=(ans<<3)+(ans<<1)+(c^48),c=getchar();
return ans;
}
inline void print(int ans){
if (ans>9) print(ans/10);
putchar(ans%10+48);
}
inline signed min(int a,int b){return a<b?a:b;}
inline signed max(int a,int b){return a>b?a:b;}
inline void swap(int &a,int &b){rr int t=a; a=b,b=t;}
inline signed mo(int x,int y){return x+y>=D?x+y-D:x+y;}
signed main(){
x0=iut(),A=iut(),B=iut(),C=iut(),
D=iut(),n=iut(),m=iut(),ti=n*m;
for (rr int i=0;i<ti;++i) a[i]=i;
for (rr int i=1;i<=ti;++i){
x0=mo(1ll*x0*mo(1ll*A*x0%D,B)%D,C);
swap(a[x0%i],a[i-1]);
}
for (rr int Q=iut();Q;--Q) swap(a[iut()-1],a[iut()-1]);
for (rr int i=0;i<ti;++i) rk[a[i]]=i;
for (rr int i=0;i<n;++i) rmin[i]=m-1;
for (rr int p=0;p<n*m;++p){
rr int x=rk[p]/m,y=rk[p]%m;
if (lmax[x]<=y&&y<=rmin[x]){
print(p+1),putchar(32),++cnt;
if (cnt==n+m-1) return 0;
for (rr int i=0;i<x;++i) rmin[i]=min(rmin[i],y);
for (rr int i=x+1;i<n;++i) lmax[i]=max(lmax[i],y);
}
}
}