BZOJ 2165 大楼
[BZOJ 2165] 大楼
题目大意
给一个带权有向图,从一个固定的起点出发,求权值和不小于定值 \(m\) 的节点最少的路径长度。
考察要点
- 动态规划
- 矩阵的倍增算法
算法讨论
Subtask 1
数据范围:\(n=2\)
分类讨论。较为麻烦,此处略。
Subtask 2
数据范围:\(m\le 3000\)
采用动态规划。
定义状态 \(f(i,j)\) 表示走到第 \(i\) 层第 \(j\) 个房间所需的最少步数。容易写出方程:
时间复杂度为 \(O\left(n^2m\right)\),可以得到 \(20\) 分。
核心代码如下:
void solve()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
scanf("%d",&w[i][j]);
memset(dp,0x3f,sizeof(dp));
dp[0][1]=0;
int ans=inf;
for(int i=0;i<=m+m;i++)
for(int j=1;j<=n;j++)
{
for(int k=1;k<=n;k++)
{
if(!w[k][j]||w[k][j]>i)continue;
dp[i][j]=min(dp[i][j],dp[i-w[k][j]][k]+1);
}
if(i>=m&&dp[i][j]!=inf)
ans=min(ans,dp[i][j]);
}
printf("%d\n",ans);
}
注意 \(i\) 这一维要放在最外层,因为 \(j\) 和 \(k\) 无遍历顺序可言,而楼层从低往高。而且 \(i\) 的枚举范围要大于 \(m\),符合题意。
Subtask 3
数据范围:若 \(w(k,j)\ne 0\) 则 \(w(k,j)\ge 10^{15}\)
此时 \(2)\) 中的算法不再适用。而所需步数在此时不会超过 \(10^3\),考虑将 \(2)\) 中状态的定义反过来。
定义状态 \(f(i,j)\) 表示走了 \(i\) 步,走到第 \(j\) 个房间能达到的最高的层数。容易写出方程:
时间复杂度 \(O\left(\dfrac{n^2m}{\min w}\right)\),可以得到 \(40\) 分。
核心代码如下:
void solve()
{
scanf("%d%lld",&n,&m);
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
scanf("%lld",&w[i][j]);
memset(dp,-0x3f,sizeof(dp));
dp[0][1]=0;
int ans=inf;
for(int i=1;i<=1000;i++)
{
for(int j=1;j<=n;j++)
{
for(int k=1;k<=n;k++)
{
if(w[k][j]==0)continue;
dp[i][j]=max(dp[i][j],dp[i-1][k]+w[k][j]);
}
if(dp[i][j]>=m)
{
ans=i;
break;
}
}
if(ans!=inf)break;
}
printf("%d\n",ans);
}
Subtask 4
数据范围:\(1\le n\le100,1\le m\le 10^{18},0\le w(i,j)\le 10^{18}\)
从 \(3)\) 出发,将状态的定义略修改为:走不超过 \(i\) 步,走到第 \(j\) 个房间能达到的最高层数。
由于我们从任意一个 \(i\) 递推到 \(i-1\) 进行的都是相同的操作,而整个递推过程具备单调性,根据转移方程定义的运算满足结合律,因此我们可以采用一种类似于矩阵乘法的倍增算法。
定义状态 \(g(p,i,j)\) 表示从房间 \(i\) 走到 \(j\),走不超过 \(2^p\) 步到达的最高层数。转移如下:
\(g(p,i,j)=\begin{cases}g(p-1,i,j)\\\max\limits_{k=1}^n\Big\{g(p-1,i,k)+g(p-1,k,j)\Big\}\end{cases}\Bigg\}\)
由于所有的边消耗的步数都为一步,因此上述倍增算法一定可以包含不超过 \(2^p\) 的所有情况。
将问题稍稍转换为:求恰好不超过 \(m\) 层所需的最少步数。将这个步数 \(+1\),就能刚好满足层数 \(\ge m\)。
我们先从前往后枚举 \(p\),找到一个 \(p\) 满足 \(\forall j\in [1,n]\),\(g[p][1][j]< m\) 且 \(\exists j\in[1,n]\),\(g[p+1][1][j]\ge m\)。然后用类似倍增 lca 的思想,从高往低位枚举 \(c\),如果当前状态再加上 \(g[c]\) 仍小于 \(m\),就将 \(g[c]\) 加上,同时将 \(ans\) 按位或上 \(2^c\),最终就能找到满足题意的答案。
代码如下:
struct matrix
{
ll v[M][M];
matrix()
{
memset(v,-0x3f,sizeof(v));
for(int i=1;i<=n;i++)v[i][i]=0;
}
inline matrix operator*(const matrix&b)const
{
matrix c;
for(int k=1;k<=n;k++)
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
{
c.v[i][j]=max(c.v[i][j],v[i][k]+b.v[k][j]);
if(c.v[i][j]>m)c.v[i][j]=m;
}
return c;
}
}dp[60],tmp;
inline bool check(matrix x)
{
for(int i=1;i<=n;i++)
if(x.v[1][i]>=m)return true;
return false;
}
void solve()
{
scanf("%d%lld",&n,&m);
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
{
ll x;scanf("%lld",&x);
if(x==0&&i!=j)dp[0].v[i][j]=-inf;
else dp[0].v[i][j]=x;
}
int cnt;
for(cnt=1;;cnt++)
{
dp[cnt]=dp[cnt-1]*dp[cnt-1];
if(check(dp[cnt]))break;
}
ll ans=0;
matrix now;
for(int i=cnt-1;i>=0;i--)
{
tmp=now*dp[i];
if(!check(tmp))
{
now=tmp;
ans+=(1ll<<i);
}
}
printf("%lld\n",ans+1);
}
注意,这里重定义了矩阵的乘法,使其既能实现转移,又能满足结合律,通过倍增来进行加速。通过矩阵加速,用类似倍增 \(+\) Floyd 的方法,我们最终将时间复杂度降为 \(O(n^3\log_2{m})\),期望得分 \(100\) 分。(实际还需卡卡常)