Luogu P6190 [NOI Online #1 入门组]魔法

思路

一、30pts做法

这种做法简直简单到爆炸,\(k==0\)的情况只要跑最短路即可。

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<cmath>
#include<queue>
#define INF 0x7fffffff
#define MAXN 2510
typedef long long ll;
int n, m, p;
ll f[MAXN][MAXN];
int main(){
    scanf("%d%d", &n, &m);
    scanf("%d", &p);
    for (int i = 1; i <= n;++i){
        for (int j = 1; j <= n;++j)
            i == j ? f[i][j] = 0 : f[i][j] = INF;
    }
    for (int i = 1; i <= m; ++i){
        int u, v, w;
        scanf("%d%d", &u, &v);
        scanf("%d", &w);
        f[u][v] = w;
    }
    for (int k = 1; k <= n;++k){
        for (int i = 1; i <= n;++i){
            for (int j = 1; j <= n;++j)
                f[i][j] = std::min(f[i][j], f[i][k] + f[k][j]);
        }
    }
    printf("%lld\n", f[1][n]);
    return 0;
}

70pts做法

关于70pts做法,其实已经和正解十分的接近,就差在了优化上(所以可见对于某些DP和搜索优化有多么重要)。

现在我们可以通过30pts的做法(直接跑最短路)知道当\(k==0\)的时候的结果。这样我们可以考虑如何从\(k==0\)的情况转移到\(k==1\)的情况。

我们设\(g[i][j][k]\)表示从第\(i\)个点到第\(j\)个点使用不超过\(k\)次魔法的情况下的最短路。那现在我们通过floyd可以求出\(g[i][j][0]\),那么怎么得到\(g[i][j][1]\)呢?

这里可以考虑遍历每一条边。由于此题\(m \leq 2500\),所以枚举并不会让你炸掉。转移时我们枚举要用魔法的边,然后在转移的时候强制走这条边,求出最短路。如果假设边\((u,v,w)\)使用了魔法,

那么这种情况先很显然转移方程就是\(g[i][j][1]=min(g[i][u][0]+g[v][j][0]-w,min(g[i][j][0],g[i][j][1]))\)

这样我们就得到了\(k==1\)的情况。现在不要着急一步得出结论,我们先来看一下从\(k==1\)如何转移到\(k==2\)

这个做法就类似于floyd。我们枚举一条从\(i\)\(j\)的边的中转点\(k\),然后在\(i\)$k$这个区间用一次魔法,在$k$\(j\)这个区间在用一次魔法(用一次魔法的转移方法与从\(k==0\)转移到\(k==1\)的方

法类似),然后将两个区间的答案和并即为\(k==2\)时的答案。这样的话转移方程即为\(g[i][j][2]=min(g[i][j][2],g[i][k][1]+g[k][j][1])\)

而根据\(k==2\)的情况,我们可以发现如果我们把最多使用\(i\)次魔法的答案和最多使用j次魔法的情况进行合并,就可以得到最多使用\(i+j\)次魔法的答案。这样对于70pts做法,我们就可以得到对于其

他情况的处理方法。我们先枚举使用魔法的次数\(s\),再枚举从点\(i\)到点\(j\)的中转点\(k\),假设在\(i\)$k$这个区间使用了$s$-$1$次魔法,在$k$\(j\)这个区间使用了\(1\)次魔法,按照这个方法进

行转移。至于为什么要这么做,我们实现时其实是先处理出\(k==1\)的情况,在再枚举\(k>1\)的情况,这样的话我们才有方法转移(如果不这样做那你还能怎么转移)。这样的话我们就可以得出最多使用\(s\)

次魔法时的转移方程为\(g[i][j][s]=min(,g[i][j][s],g[i][k][s-1]+g[k][j][1])\)

Code

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<cmath>
#include<queue>
#define INF 0x3f
#define MAXN 110
using std::cout;
typedef long long ll;
int n, m, p;
ll f[MAXN][MAXN], g[MAXN][MAXN][MAXN];
struct node{
    int u, v, w;
} edge[2550];//由于之后的枚举中要用到边的信息,所以我们要把边的信息存储下来
int main(){
    scanf("%d%d", &n, &m);
    scanf("%d", &p);
    std::memset(f, 0x3f, sizeof(f));
    std::memset(g, 0x3f, sizeof(g));
    for(int i=1;i<=n;++i)f[i][i]=0;//这里注意每个点到自己的距离为0
    for (int i = 1; i <= m; ++i){
        scanf("%d%d", &edge[i].u, &edge[i].v);
        scanf("%d", &edge[i].w);
        f[edge[i].u][edge[i].v] = edge[i].w;//把边的信息存到f数组中,方便DP使用
    }
    for (int k = 1; k <= n;++k){
        for (int i = 1; i <= n;++i){
            for (int j = 1; j <= n;++j)
                f[i][j] = std::min(f[i][j], f[i][k] + f[k][j]), g[i][j][0] = f[i][j];
        }//最普通的floyd,求当k==0时的情况
    }
    for (int k = 1; k <= m; ++k){
        int u = edge[k].u, v = edge[k].v, w = edge[k].w;
        for (int i = 1; i <= n; ++i){
            for (int j = 1; j <= n; ++j)//这里要先处理出k==1是的情况,方便k>1的情况的转移
                g[i][j][1] = std::min(g[i][u][0] + g[v][j][0] - w, std::min(g[i][j][0], g[i][j][1]));
        }
    }
    for (int s = 1; s <= p;++s){
        for (int k = 1; k <= n;++k){
            for (int i = 1; i <= n;++i){
                for (int j = 1; j <= n;++j)
                    g[i][j][s] = std::min(g[i][j][s], g[i][k][s - 1] + g[k][j][1]);
            }
        }
    }
    printf("%lld\n", g[1][n][p]);//输出
    return 0;
}

100pts做法

虽然这个做法说是对70pts做法的优化,其实差别还是不小的。100pts做法需要的是将矩阵乘法的乘改为取min。而且,因为我们是要对不同的方案合并,所以还要证明这么做满足结合律。

现在进入正题……首先我们先要来说一下这道题的前置知识——矩阵快速幂。矩阵快速幂,顾名思义,就是用矩阵优化的快速幂(我感觉这说了跟没说一样)。
1.快速幂(注:因为时间关系,下列内容除代码外都来自网络,望大家谅解)

快速幂其实说白了就会让你的乘方算法变得更快(而且不是快一星半点)。

该怎样去加速幂运算的过程呢?既然我们觉得将幂运算分为\(n\)步进行太慢,那我们就要想办法减少步骤,把其中的某一部分合成一步来进行。

比如,如果\(n\)能被\(2\)整除,那我们可以先计算一半,得到\(a^{n/2}\)的值,再把这个值平方得出结果。这样做虽然有优化,但优化的程度很小,仍是线性的复杂度。

再比如,如果我们能找到\(2^k=n\),我们就能把原来的运算优化成:\(((a^2)^2)^2...\),只需要\(k\)次运算就可以完成,效率大大提升。可惜的是,这种条件显然太苛刻了,适用范围很小。不过这给了我们一

种思路,虽然我们很难找到\(2^k=n\),但我们能够找到\(2^{k_1}+2^{k_2}+2^{k_3}+......+2^{k_m}=n\)。这样,我们可以通过递推,在很短的时间内求出各个项的值。

我们都学习过进制与进制的转换,知道一个\(b\)进制数的值可以表示为各个数位的值与权值之积的总和。比如,2进制数\(10011001\),它的值可以表示为10进制的\(1\times 2^3+0\times 2^2+0\times 2^1+1\times 2^0\),即9。

这完美地符合了上面的要求。可以通过2进制来把\(n\)转化成\(2^{k_m}\)的序列之和,而2进制中第\(i\)位(从右边开始计数,值为1或是0)则标记了对应的\(2^{i−1}\)是否存在于序列之中。

譬如,13为二进制的\(11011101\),他可以表示为\(2^3+2^2+2^0\),其中由于第二位为0,\(2^1\)项被舍去。

如此一来,我们只需要计算\(a\)\(a^2\)\(a^4\)\(a^8\)......的值(这个序列中的项不一定都存在,由\(n\)的二进制决定)并把它们乘起来即可完成整个幂运算。借助位运算的操作,可以很方便地

实现这一算法,其复杂度为\(O(log_2 n)\)

2.矩阵快速幂

(1)矩阵乘法

简单的说矩阵就是二维数组,数存在里面,矩阵乘法的规则:A*B=C

一个nm的矩阵可以和一个ma的矩阵相乘得到一个n*a的矩阵。

对于结果C,满足:

不理解?我们可以举个栗子:

矩阵乘法也就这么多,没有太多可以赘述的。

(2)矩阵快速幂

矩阵快速幂,说白了就是把快速幂里的乘法换成矩阵乘法就行了。由矩阵乘法的定义可知,如果a*a合法,那么a的行等于a的列,所以快速幂的矩阵必须是方阵(行和列相等)。要找到一个方阵a,

使与a边长相同的矩阵乘a结果等于它本身。这里的a只有对角线是1,其他的值都为0,这时能保证与a边长相同的矩阵乘a结果等于它本身。

对于矩阵快速幂的代码,由于时间原因,我没法都写出来,这部分的代码在下面的满分代码中会有涉及。

那么,说了这么多,此题应该如何使用矩阵快速幂来优化呢?

对于一个邻接矩阵P,那么\(P^k\)表示的就是恰好经过k步后的状态。摘自题解。

假设我们有一个矩阵P,P(i,j)表示的就是在1步内从i到j的方案数。很显然,这个矩阵内只会有0,1两种数。对这个矩阵做k遍矩阵乘法之后,得到的矩阵P’就是恰好走了k步时从i到j的方案数。
举个例子:

假设原矩阵为:

我们对他进行3次矩阵乘法
第一次得到:

第二次得到:

第三次得到:

这样应该就会很形象生动了……在不懂的话可以自己手推一下。

这么做的话其实就可以用矩阵快速幂来完成一部分的动态规划,以达到节省时间的目的。只是为了满足题目的要求,这里的矩阵乘法的定义需要改变,即把乘法改为取min的操作。至于为什么这样做可

以,我也不太清楚(我太菜了)。

Code

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<cmath>
#define MAXN 110
#define INF 1e15+100
typedef long long ll;
int n, m, p;
ll f[MAXN][MAXN];
struct node{
    int u, v, w;
} edge[2550];
struct Matrix{
    ll a[MAXN][MAXN];
} g;//这里把矩阵存放在结构体里,方便快速幂之后返回结果
Matrix operator*(const Matrix &a,const Matrix &b){//重载运算符
    Matrix res;
    for (int i = 1; i <= n;++i){
        for (int j = 1; j <= n;++j)
            res.a[i][j] = INF;
    }//初始化res数组,全部赋值为INF,因为下面取最小值
    for (int k = 1; k <= n;++k){
        for (int i = 1; i <= n;++i){
            for (int j = 1; j <= n;++j)
                res.a[i][j] = std::min(res.a[i][j], a.a[i][k] + b.a[k][j]);
        }//这里的做法和floyd的写法极其类似,这里也就是相当于完成矩阵乘法
    }
    return res;
}
Matrix Quick_pow(Matrix a,int k){//矩阵快速幂
    Matrix res;
    for (int i = 1; i <= n;++i){
        for (int j = 1; j <= n;++j)
            res.a[i][j] = f[i][j];
    }//先把f数组的值存储到res里,方便操作
    while(k){
        if(k&1)//若二进制k最后一位为1
            res = res * a;//做矩阵乘法
        a = a * a;//对a平方
        k >>= 1;//把二进制k的最后一位舍掉
    }
    return res;
}
int main(){
    scanf("%d%d",&n,&m);
    scanf("%d", &p);
    memset(f, 0x3f, sizeof(f));
    memset(g.a, 0x3f, sizeof(g.a));
    for (int i = 1; i <= n;++i)
        f[i][i] = 0;//初始化
    for (int i = 1; i <= m; ++i){
        scanf("%d%d", &edge[i].u, &edge[i].v);
        scanf("%d", &edge[i].w);
        f[edge[i].u][edge[i].v] = edge[i].w;
    }//读入
    for (int k = 1; k <= n;++k){
        for (int i = 1; i <= n;++i){
            for (int j = 1; j <= n;++j)
                f[i][j] = std::min(f[i][j], f[i][k] + f[k][j]);
        }
    }//floyd部分
    if(p==0){
        printf("%lld\n", f[1][n]);
        return 0;
    }//这里需要特判,因为任何数的0次幂都等于1,如果直接做矩阵乘法会炸
    for (int k = 1; k <= m;++k){
        int u = edge[k].u, v = edge[k].v, w = edge[k].w;
        for (int i = 1; i <= n;++i){
            for (int j = 1; j <= n;++j)
                g.a[i][j] = std::min(f[i][u] + f[v][j] - w, std::min(g.a[i][j], f[i][j]));
        }//这里处理出k==1时的结果,完善矩阵快速幂所需要的矩阵
    }
    printf("%lld\n", Quick_pow(g, p).a[1][n]);//输出
    return 0;
}
posted @ 2020-07-28 09:30  Shadow_hyc  阅读(166)  评论(0)    收藏  举报