BZOJ 2165 大楼

[BZOJ 2165] 大楼


题目大意

给一个带权有向图,从一个固定的起点出发,求权值和不小于定值 \(m\) 的节点最少的路径长度。


考察要点

  • 动态规划
  • 矩阵的倍增算法

算法讨论

Subtask 1

数据范围:\(n=2\)

分类讨论。较为麻烦,此处略。


Subtask 2

数据范围:\(m\le 3000\)

采用动态规划。

定义状态 \(f(i,j)\) 表示走到第 \(i\) 层第 \(j\) 个房间所需的最少步数。容易写出方程:

\[f(i,j)=\min\limits_{k=1}^n{\Big\{f[i-w(k,j),k]+1,(k,j)\in E\Big\}} \]

时间复杂度为 \(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\) 个房间能达到的最高的层数。容易写出方程:

\[f(i,j)=\max\limits_{k=1}^n{\Big\{f(i-1,k)+w(k,j),(k,j)\in E\Big\}} \]

时间复杂度 \(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\) 分。(实际还需卡卡常)

posted @ 2021-07-01 17:02  cyl06  阅读(58)  评论(0编辑  收藏  举报