[SMOJ2091]短路

97 篇文章 0 订阅
5 篇文章 0 订阅

30%:

暴搜+玄学剪枝?(没试过,不知道会不会 T)

n5 则最多有 121 个点,建个图跑个最短路应该还是可以的。

50%:

结论一:最优情况应该只向下或向右走,则可以做一个 O(n2) 的 dp。 f[i][j] 表示从左上角走到点 (i,j) 的最小用时,显然有

f[i][j]=min{f[i1][j],f[i][j1]}+a[max{nmin{i1,2n+1i},nmin{j1,2n+1j}}]

参考代码:

#include <algorithm>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <iostream>

using namespace std;

const int MAXN = 1005;

int n, p[(MAXN << 1) + 1];
long long a[MAXN];
long long f[(MAXN << 1) + 1][(MAXN << 1) + 1];

int main(void) {
    freopen("2091.in", "r", stdin);
    freopen("2091.out", "w", stdout);
    scanf("%d", &n);
    for (int i = 0; i <= n; i++) scanf("%lld", &a[i]);
    int l = (n << 1) + 1;
    for (int i = 1; i <= l; i++) p[i] = min(i - 1, l - i);
//  for (int i = 2; i <= l; i++) f[0][i] = f[i][0] = 1000000001LL;
    for (int i = 1; i <= l; i++) {
        for (int j = 1; j <= l; j++) {
            long long v = a[n - min(p[i], p[j])];
//          printf("%lld ", v);
            if (i == 1) f[i][j] = v + f[i][j - 1];
            else if (j == 1) f[i][j] = v + f[i - 1][j];
            else f[i][j] = v + min(f[i - 1][j], f[i][j - 1]);
//          printf("%lld ", f[i][j]);
        }
//      putchar('\n');
    }
    printf("%lld\n", f[l][l]);
    return 0;
}


100%:

贪心。

首先需要证明上面猜想的正确性。

事实上,对于一条存在向上或向左走的路径,我们总能够找到至少一种方法,通过平移边的方式,得到另一种等价甚至更优的方案。例如:

对于上图中的这种方案,出现了从右边走到左边的情况。不难发现对于同一层的连通器,一条边的费用是一定的,则将最右边的边平移到最左边不影响总费用。而内层与外层连通需要 +3s,将那条边也向左移,之后发现用这两条边已经可以连通了,可以去掉上面的两条 +4s 边,变成:

这样我们就可以通过平移调整,得到一种不会变得更差的方案。类似地,所有出现向左走和向上走的情况都可以这样处理。因此我们认为一定存在某种最优方案,其中每步要么向下走,要么向右走。

结论二:根据结论一,走到 (i,i) 的位置需要恰好 2(i1) 步,其中前 (i1) 层中每层至少走一步,剩下 (i1) 步暂时不确定。

理由: (i,i) 与 (1,1) 的曼哈顿距离为 |i1|+|i1|=2(i1) ;又因为不可能从外面跳跃一层直接到达里面的层,因此前 (i1) 层中每层至少需要走一步。

(接下来的部分参考了 myy 的官方题解)

结论三:剩下来的 (i1) 步至少要走的是 1 到 (i1) 层的前缀最小值之和。

证明:

根据结论一,对于每一个点 (i,j) ,要么是从 (i1,j) 走下来的,要么是从 (i,j1) 向右走过来的。对于后者,我们总是可以把这最后一步挪到前面去走,相当于在前面的第 k 层先走了这一步,然后把这一步之后的路径平移一下(参见证明结论一时移动纵向边的过程),而被平移这段路径对总答案的贡献不会受到影响,这样代价变化就是第 x 层的 a 与第 k 层的 a 的差值。

由此可得,如果第 x 层的 a 不是前缀最小值,那么肯定不是最短路径。即,最短路径不会在不是前缀最小值的层停留超过 1 次,于是最短路就是把所有的前缀最小值所在层的最左上角的点用 L 型的曲线连起来的路径。

结论四:设最深走到第 x 层,容易证明,如果第 x 层不是前缀最小值,那么不可能是最短路径;否则可以从第 x 层的左上角沿着第 x 层绕一圈到达该层的右下角,再走一条跟来的时候一样的路到达最右下角(注意利用整块主板的对称性质)。

这样就得到了一种基于枚举的贪心算法:从外到内计算并同时维护前缀最小值,若当前为前缀最小值则尝试更新答案。时间复杂度为 O(n)

另外这题要注意一个地方,输入数据的 a 从内到外给出的,要细心一点。

我赛后重写的时候就忘了这个地方,半天过不了样例。后来又把初始答案时的 4n+1 写成了 4n1 ,导致只有 90 分。做题一定要注意细节。

参考代码:

#include <algorithm>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <iostream>

using namespace std;

const int MAXN = 1e5 + 100;

int n;
long long a[MAXN], f[MAXN];

int main(void) {
    freopen("2091.in", "r", stdin);
    freopen("2091.out", "w", stdout);
    scanf("%d", &n);
//  long long a, Min; scanf("%lld", &a);
    for (int i = n; i >= 0; i--) scanf("%lld", &a[i]);
    long long Min; //前缀最小值
    f[0] = Min = a[0];
    long long ans = a[0] * ((n << 2) + 1); //直接从最外层走 L 字形到右下角的情况
//  printf("%lld\n", ans);
    for (int i = 1; i <= n; i++) {
//      scanf("%lld", &a);
        f[i] = f[i - 1] + Min + a[i];
        if (Min >= a[i]) { //当前为前缀最小值
            ans = min(ans, ((f[i] + a[i] * ((n - i) << 1)) << 1) - a[i]); //对称,所以从最左上走到这层的左上跟从这层的右下走到最右下是一样的
            Min = a[i];
        }
//      printf("%lld\n", ans);
    }
    printf("%lld\n", ans);
    return 0;
}


这两天自己做题和改题总是在小地方犯一些不必要的错误,自己也反思了一下。就像 cdc 告诉我们,开始打代码前先自己大致列一下框架。很多时候欲速则不达,而理清楚思路实际上也并不会花太长时间,但可以做到“磨刀不误砍柴工”。把应该做的事情做好,有成竹在胸的自信,很多东西自然就会水到渠成了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值