【C++】NOI Online能力测试 入门组题目+解析+代码

本文深入解析了NOIOnline平台上的三道经典算法题目:文具订购、跑步计划与魔法之旅,涵盖模拟、动态规划与图论算法,提供多种解题思路及代码实现。

文具订购

【题目描述】

小明的班上共有 n 元班费,同学们准备使用班费集体购买 3 种物品:
1.圆规,每个 7 元。
2.笔,每支 4 元。
3.笔记本,每本 3 元。
小明负责订购文具,设圆规,笔,笔记本的订购数量分别为 a,b,c,他订购的原则依次如下:
1.n 元钱必须正好用光,即 7a+4b+3c=n。
2.在满足以上条件情况下,成套的数量尽可能大,即 a,b,c 中的最小值尽可能大。
3.在满足以上条件情况下,物品的总数尽可能大,即 a+b+c 尽可能大。
请你帮助小明求出满足条件的最优方案。可以证明若存在方案,则最优方案唯一。

【输入格式】

从文件 order.in 中读入数据。
仅一行一个整数 n 表示班费数量。

【输出格式】

输出到文件 order.out 中。
若方案不存在则输出 -1。否则输出一行三个用空格分隔的非负整数 a,b,c 表示答案。

【样例1输入】

1

【样例1输出】

-1

【样例2输入】

14

【样例2输出】

1 1 1

【样例3输入】

33

【样例3输出】

1 2 6

【样例3解释】

a=2,b=4,c=1 也是满足条件 1,2 的方案,但对于条件 3,该方案只买了 7 个物品,不如 a=1,b=2,c=6 的方案。

【数据范围与提示】

对于测试点 1 ∼ 6:n ≤ 14。
对于测试点 7 ∼ 12:n 是 14 的倍数。
对于测试点 13 ∼ 18:n ≤ 100。
对于所有测试点:0 ≤ n ≤ 105。

【时间限制】

1.0s

【空间限制】

256MB

【上传文件】

上传c, cpp或pas语言源程序,文件名应依次为order.c, order.cpp, order.pas。

【解析1】

模拟。三重循环。但这不是简单的三重循环。具体看代码。

【代码1】

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

#define R                  register int
#define re(i,a,b)          for(R i=a; i<=b; i++)
#define ms(i,a)            memset(a,i,sizeof(a))
#define MAX(a,b)           (((a)>(b)) ? (a):(b))
#define MIN(a,b)           (((a)<(b)) ? (a):(b))

using namespace std;

typedef long long LL;

int const N=105;

int n;

int main() {
    //freopen("order.in","r",stdin);
    //freopen("order.out","w",stdout);
    scanf("%d",&n); 
    if(n==0) {
        printf("0 0 0\n");
        //fclose(stdin);
        //fclose(stdout);
        return 0;
    } else {
        for(int i=n/14; i>=0; i--) {  //枚举a
            for(int j=i; j<=n/4; j++) //枚举b
                for(int k=i; k<=n/3; k++) //枚举c
                    if(i*7+j*4+k*3==n) {  //判断条件是否满足
                        printf("%d %d %d\n",i,j,k);
	                    //fclose(stdin);
	                    //fclose(stdout);
                        return 0;
                    }
        }
    }
    printf("-1\n");
    //fclose(stdin);
    //fclose(stdout);
    return 0;
}

【解析2】

模拟。当然,暴力的三重循环可以优化到两重。这种方法的时间比较费。有点悬,所以开了快读。

【代码2】

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

#define R                  register int
#define re(i,a,b)          for(R i=a; i<=b; i++)
#define ms(i,a)            memset(a,i,sizeof(a))
#define MAX(a,b)           (((a)>(b)) ? (a):(b))
#define MIN(a,b)           (((a)<(b)) ? (a):(b))

using namespace std;

typedef long long LL;

int const N=105;

int n,vis;
int ansa,ansb,ansc,ansmin,anstot;

inline void read(int &x){
	x=0; 
	char c=0; 
	int w=0;  
	while (!isdigit(c)) w|=c=='-',c=getchar();  
	while (isdigit(c)) x=x*10+(c^48),c=getchar();  
	if(w) x=-x;  
}

inline int min3(int a,int b,int c) {
    return MIN(MIN(a,b),c);
}

int main() {
    //freopen("order.in","r",stdin);
    //freopen("order.out","w",stdout);
    read(n); //读入
    for(int a=0; a*7<=n; a++) {
        int bb=n-7*a; //bb为b的上限
        for(int b=0; b*4<=bb; b++) {
            int c=n-7*a-4*b;
            if(c%3) continue;
            vis=1;  //标记有解
            c=c/3;
            int nowmin=min3(a,b,c);
            int nowtot=a+b+c;
            if(nowmin>ansmin) { //判断条件1
                ansa=a,ansb=b,ansc=c;
                ansmin=nowmin,anstot=nowtot;
            } else if(nowmin==ansmin) {  //判断条件2
                if(nowtot>anstot) {
                    ansa=a,ansb=b,ansc=c;
                    ansmin=nowmin,anstot=nowtot;
                }
            }
        }
    }
    if(vis) printf("%d %d %d\n",ansa,ansb,ansc);
        else printf("-1\n");
    //fclose(stdin);
    //fclose(stdout);
    return 0;
}

【解析3】

(解析3来自 栾竹清影的博客,原文链接 ,有删改)

这个算法有点难以置信,但是对的。时间复杂度O(1)。

我们一个一个条件地分析:

1 要花光所有的钱:
首先,三种文具的价格是 3,4,73,4,7 ,通过证明,发现除了 n=1 ,n=2 , n=5 以外,任何的价格 n 都可以被花光,所以这个条件并没有什么太大的约束力;

2 尽量配成更多套

一套的价钱是 1414 元,所以这个规定就是要我们尽可能地买更多的整 1414 元;

3 尽量买更多的物品 :

这个规定其实就是在我们买了尽可能多的整 14 元之后,剩余的钱再尽量拆分成 3 元或 44 元(因为 7=3+4 ,这样买的物品数量就会减少,所以不买 7 元的东西)

好了,题目分析完了,我们归纳一下:

ans=n/14;

n%=14;

if(n==?) {
	cout<<ans<<" "<<ans+?<<" "<<ans+?;
	return 0;
}

if(n==?)

    ·
    ·
    ·
    ·
    

(注:由于是伪代码,所以?处是代表一个数字)

看见了吗?其实这道题就是一个除法,再加上好多个 ifif 就解决了。。。

但是细心的朋友可能会发现,这个算法有一个漏洞:

如果买完尽量多的套数以后,剩下的是 1,2,51,2,5 元呢?这样不就不满足第一个条件了吗?

不用担心,这个问题很简单,我们只需要少配一套物品,再拿出14元来,不就可以花完钱了吗?

【代码3】

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

#define R                  register int
#define re(i,a,b)          for(R i=a; i<=b; i++)
#define ms(i,a)            memset(a,i,sizeof(a))
#define MAX(a,b)           (((a)>(b)) ? (a):(b))
#define MIN(a,b)           (((a)<(b)) ? (a):(b))

using namespace std;

typedef long long LL;

int const N=105;

int n,ans;
//ans记录套数

int main() {
    //freopen("order.in","r",stdin);
    //freopen("order.out","w",stdout);
    scanf("%d",&n);
    if(n==0) {
        printf("0 0 0\n");
        //fclose(stdin);
        //fclose(stdout);
        return 0;
    }
    if(n<6 && n!=4 && n!=3) {  ////若n无法分完,则直接输出答案
        printf("-1\n");
        //fclose(stdin);
        //fclose(stdout);
        return 0;
    }
    ans=n/14;
    n=n%14;
    if(n<6 && n!=4 && n!=3 && n!=0) 
        ans--,n=n+14;  //若在套数最多时n分不完,套数-1
    if(n==0) {  //若正好分完,直接输出三个套数
        printf("%d %d %d\n",ans,ans,ans);
        //fclose(stdin);
        //fclose(stdout);
        return 0;
    }
    if(n==3) printf("%d %d %d\n",ans,ans,ans+1);
        else if(n==4) printf("%d %d %d\n",ans,ans+1,ans);
            else if(n==6) printf("%d %d %d\n",ans,ans,ans+2);
                else if(n==7) printf("%d %d %d\n",ans,ans+1,ans+1);
                    else if(n==8) printf("%d %d %d",ans,ans+2,ans);
                        else if(n==9) printf("%d %d %d\n",ans,ans,ans+3);
                            else if(n==10) printf("%d %d %d\n",ans,ans+1,ans+2);
                                else if(n==11) printf("%d %d %d\n",ans,ans+2,ans+1);
                                    else if(n==12) printf("%d %d %d\n",ans,ans,ans+4);
                                        else if(n==13) printf("%d %d %d\n",ans,ans+1,ans+3);
                                            else if(n==15) printf("%d %d %d\n",ans,ans,ans+5);
                                                else if(n==16) printf("%d %d %d\n",ans,ans+1,ans+4);
                                                    else if(n==19) printf("%d %d %d\n",ans,ans+1,ans+5);
    //fclose(stdin);
    //fclose(stdout);
    return 0;
}

if-else也可以替换为switch-case:

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

#define R                  register int
#define re(i,a,b)          for(R i=a; i<=b; i++)
#define ms(i,a)            memset(a,i,sizeof(a))
#define MAX(a,b)           (((a)>(b)) ? (a):(b))
#define MIN(a,b)           (((a)<(b)) ? (a):(b))

using namespace std;

typedef long long LL;

int const N=105;

int n,ans;
//ans记录套数

int main() {
    //freopen("order.in","r",stdin);
    //freopen("order.out","w",stdout);
    scanf("%d",&n);
    if(n==0) {
        printf("0 0 0\n");
        //fclose(stdin);
        //fclose(stdout);
        return 0;
    }
    if(n<6 && n!=4 && n!=3) {  ////若n无法分完,则直接输出答案
        printf("-1\n");
        //fclose(stdin);
        //fclose(stdout);
        return 0;
    }
    ans=n/14;
    n=n%14;
    if(n<6 && n!=4 && n!=3 && n!=0) 
        ans--,n=n+14;  //若在套数最多时n分不完,套数-1
    if(n==0) {  //若正好分完,直接输出三个套数
        printf("%d %d %d\n",ans,ans,ans);
        //fclose(stdin);
        //fclose(stdout);
        return 0;
    }
    switch(n) { 
        case 3: printf("%d %d %d\n",ans,ans,ans+1); break;
        case 4: printf("%d %d %d\n",ans,ans+1,ans); break;
        case 6: printf("%d %d %d\n",ans,ans,ans+2); break;
        case 7: printf("%d %d %d\n",ans,ans+1,ans+1); break;
        case 8: printf("%d %d %d",ans,ans+2,ans); break;
        case 9: printf("%d %d %d\n",ans,ans,ans+3); break;
        case 10: printf("%d %d %d\n",ans,ans+1,ans+2); break;
        case 11: printf("%d %d %d\n",ans,ans+2,ans+1); break;
        case 12: printf("%d %d %d\n",ans,ans,ans+4); break;
        case 13: printf("%d %d %d\n",ans,ans+1,ans+3); break;
        case 15: printf("%d %d %d\n",ans,ans,ans+5); break;
        case 16: printf("%d %d %d\n",ans,ans+1,ans+4); break;
        case 19: printf("%d %d %d\n",ans,ans+1,ans+5); break;
    }
    //fclose(stdin);
    //fclose(stdout);
    return 0;
}


跑步

【题目描述】

小 H 是一个热爱运动的孩子,某天他想给自己制定一个跑步计划。小 H 计划跑 n 米,其中第 i(i ≥ 1) 分钟要跑 xi米(xi 是正整数),但没有确定好总时长。由于随着跑步时间增加,小 H 会越来越累,所以小 H 的计划必须满足对于任意 i(i>1) 有 xi ≤ xi-1。现在小 H 想知道一共有多少个不同的满足条件的计划,请你帮助他。两个计划不同当且仅当跑步的总时长不同,或者存在一个 i,使得两个计划中 xi 不相同。由于最后的答案可能很大,你只需要求出答案对 p 取模的结果。

【输入格式】

从文件 running.in 中读入数据。
仅一行两个整数 n,p 表示跑步距离与模数。

【输出格式】

输出到文件 running.out 中。
仅一行一个整数,表示答案模 p 的值。

【样例1输入】

4 44

【样例1输出】

5 

【样例1解释】

五个不同的计划分别是:{1,1,1,1},{2,1,1},{3,1},{2,2},{4}。

【样例2输入】

66 666666

【样例2输出】

323522

【样例3输入】

66666 66666666

【样例3输出】

45183149

【数据范围与提示】

对于所有测试点:1 ≤ n ≤ 105, 1 ≤ p < 230。
每个测试点限制具体如下:

测试点编号n ≤
15
210
350
4100
5500
62000
75000
820000
950000
10100000

【时间限制】

2.0s

【空间限制】

256MB

【上传文件】

上传c, cpp或pas语言源程序,文件名应依次为running.c, running.cpp, running.pas。

【解析1】

70分的做法。
题目的实质是整数拆分。
球盒问题。
n个相同球放到m个相同的盒子里(无空箱)。
(球盒问题总结:点击这里
d p [ n ] [ m ] = d p [ n ] [ m − 1 ] + d p [ n − m ] [ m ] ( n > = m ) dp[n][m]=dp[n][m−1]+dp[n−m][m] (n>=m) dp[n][m]=dp[n][m1]+dp[nm][m](n>=m)
d p [ n ] [ m ] = d p [ n ] [ m − 1 ] ( n < m ) dp[n][m]=dp[n][m−1](n<m) dp[n][m]=dp[n][m1](n<m)
边 界 : d p [ k ] [ 1 ] = 1 边界:dp[k][1]=1 dp[k][1]=1
a n s = S i g m a f [ n ] [ i ] ( i = 1 , i < = n ) ans=Sigma f[n][i] (i=1,i<=n) ans=Sigmaf[n][i](i=1,i<=n)

【代码1】

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

#define R                  register int
#define re(i,a,b)          for(R i=a; i<=b; i++)
#define ms(i,a)            memset(a,i,sizeof(a))
#define MAX(a,b)           (((a)>(b)) ? (a):(b))
#define MIN(a,b)           (((a)<(b)) ? (a):(b))

using namespace std;

typedef long long LL;

int const N=5005;

int n,MOD,ans;
int f[N][N];

void fun(int n) {
    ms(0,f);
    for(int i=1; i<=n; i++) f[i][1]=1;
	for(int i=2; i<=n; i++) for(int j=2; j<=i; j++) {
        f[i][j]=(f[i-1][j-1]+f[i-j][j])%MOD;
    }
}

int main() {
    //freopen("running.in","r",stdin);
    //freopen("running.out","w",stdout);
    scanf("%d%d",&n,&MOD);
    fun(n);
    for(int i=1; i<=n; i++) ans=ans+f[n][i],ans=ans%MOD;
    printf("%d\n",ans);
	//fclose(stdin);
	//fclose(stdout);
    return 0;
}

【解析2】

100分的做法。
首先要了解五边形数五边形数定理

题解在此:整数拆分

完。

【代码2】

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

#define R                  register int
#define re(i,a,b)          for(R i=a; i<=b; i++)
#define ms(i,a)            memset(a,i,sizeof(a))
#define MAX(a,b)           (((a)>(b)) ? (a):(b))
#define MIN(a,b)           (((a)<(b)) ? (a):(b))

using namespace std;

typedef long long LL;

int const N=100005;

int n,MOD,ans;
int f[N];

inline int fun(int x) {
    int ans=(3*x*x-x)>>1;
    return ans;
}

int main() {
    //freopen("running.in","r",stdin);
    //freopen("running.out","w",stdout);
    scanf("%d%d",&n,&MOD);
    f[0]=1;
    for(int i=1; i<=n; i++) {
        for(int j=1; ; j++) {
            int x=fun(j),y=fun(-j);
            if(x<=i) f[i]=((f[i]+(j&1 ? 1:-1)*f[i-x])%MOD+MOD)%MOD;
            if(y<=i) f[i]=((f[i]+(j&1 ? 1:-1)*f[i-y])%MOD+MOD)%MOD;
            if(x>i || y>i) break;
        }
    }
    printf("%d\n",f[n]);
	//fclose(stdin);
	//fclose(stdout);
    return 0;
}

魔法

【题目描述】

C 国由 n 座城市与 m 条有向道路组成,城市与道路都从 1 开始编号,经过 i 号道路需要 ti 的费用。
现在你要从 1 号城市出发去 n 号城市,你可以施展最多 K 次魔法,使得通过下一条道路时,需要的费用变为原来的相反数,即费用从 ti 变为 -ti。请你算一算,你至少要花费多少费用才能完成这次旅程。注意:使用魔法只是改变一次的花费,而不改变一条道路自身的 ti;最终的费用可以为负,并且一个城市可以经过多次(包括 n 号城市)。

【输入格式】

从文件 magic.in 中读入数据。
第一行三个整数 n, m, K,表示城市数、道路数、魔法次数限制。
接下来 m 行每行三个整数 ui,vi,ti,第 i 行描述 i 号道路,表示一条从 ui 到 vi 的有向道路,经过它需要花费 ti。

【输出格式】

输出到文件 magic.out 中。
仅一行一个整数表示答案。

【样例1输入】

4 3 2
1 2 5
2 3 4
3 4 1

【样例1输出】

-8

【样例1解释】

依次经过 1 号道路、2 号道路、3 号道路,并在经过 1、2 号道路前使用魔法。

【样例2输入】

2 2 2
1 2 10
2 1 1

【样例2输出】

-19

【样例2解释】

依次经过 1 号道路、2 号道路、1 号道路,并在两次经过 1 号道路前都使用魔法。

【数据范围与提示】

对于所有测试点和样例满足:
1 ≤ n ≤ 100,1 ≤ m ≤ 2500,0 ≤ K ≤ 106,1 ≤ ui,vi ≤ n,1 ≤ ti ≤ 109。
数据保证图中无自环,无重边,至少存在一条从 1 号城市到达 n 号城市的路径。
每个测试点的具体限制见下表。

测试点编号n ≤m ≤K ≤特殊限制
1 ∼ 25200
3 ∼ 4102050
5 ∼ 610200
7 ∼ 82020050图中无环
9 ∼ 10202000
11 ∼ 1210020050图中无环
13 ∼ 1410020050
15 ∼ 1810025001000
19 ∼ 201002500106

【时间限制】

1.0s

【空间限制】

256MB

【上传文件】

上传c, cpp或pas语言源程序,文件名应依次为magic.c, magic.cpp, magic.pas。

【解析1】

30分的做法。
看到这么复杂的题目和超大的数据,不会?
没有关系。
静下心来,仔细看一下数据。
你会发现有6组数据k=0。
先用Floyd骗个30分吧。

【代码1】

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

#define R                  register int
#define re(i,a,b)          for(R i=a; i<=b; i++)
#define ms(i,a)            memset(a,i,sizeof(a))
#define MAX(a,b)           (((a)>(b)) ? (a):(b))
#define MIN(a,b)           (((a)<(b)) ? (a):(b))

using namespace std;

typedef long long LL;

LL const inf=1e16;
int const N=105;

int n,m,k;
LL f[N][N];

int main() {
    //freopen("magic.in","r",stdin);
    //freopen("magic.out","w",stdout);
    scanf("%d%d%d",&n,&m,&k);
    for(int i=1; i<=100; i++) for(int j=1; j<=100; j++)
        f[i][j]=inf;
	for(int i=1; i<=m; i++) {
        int t,u,v;
        scanf("%d%d%d",&u,&v,&t);
        f[u][v]=t;
    }
    for(int k2=1; k2<=n; k2++) for(int i=1; i<=n; i++) for(int j=1; j<=n; j++) 
        f[i][j]=MIN(f[i][j],f[i][k2]+f[k2][j]);
    printf("%lld\n",f[1][n]);
	//fclose(stdin);
	//fclose(stdout);
    return 0;
}

【解析2】

70分的做法。
spfa。

【代码2】

#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
#include <algorithm>
#include <queue>

#define R                  register int
#define re(i,a,b)          for(R i=a; i<=b; i++)
#define ms(i,a)            memset(a,i,sizeof(a))
#define MAX(a,b)           (((a)>(b)) ? (a):(b))
#define MIN(a,b)           (((a)<(b)) ? (a):(b))

using namespace std;

typedef long long LL;
typedef pair<int,int> P;

int const N=105;
int const M=1e6+5;
int const K=2e5+5;
LL const inf=1e16;

int n,m,k;
int h[N];
int vis[N][K];
LL d[N][K];

queue<P> q;

struct Edge {
    int to,nt,len;
} e[M];

void spfa() {
    for(int i=1; i<=n; i++) for(int j=0; j<=k; j++) d[i][j]=inf;
    d[1][0]=0;
    q.push(P(1,0));
    vis[1][0]=1;
    while(!q.empty()) {
        int a=q.front().first;
        int x=q.front().second;
        q.pop();
        vis[a][x]=0;
        for(int i=h[a]; i; i=e[i].nt) {
            int y=e[i].to,z=e[i].len;
            if(d[y][x]>d[a][x]+z) {
                d[y][x]=d[a][x]+z;
                if(!vis[y][x]) {
                    q.push(P(y,x));
                    vis[y][x]=1;
                }
            }
            if(x<k && d[y][x+1]>d[a][x]-z) {
                d[y][x+1]=d[a][x]-z;
                if(!vis[y][x+1]) {
                    q.push(P(y,x+1));
                    vis[y][x+1]=1;
                }
            }
        }
    }
}

int main() {
    //freopen("magic.in","r",stdin);
    //freopen("magic.out","w",stdout);
    scanf("%d%d%d",&n,&m,&k);
    for(int i=1; i<=m; i++) {
        int x,y,z;
        scanf("%d%d%d",&x,&y,&z);
        e[i]=(Edge){y,h[x],z};
        h[x]=i;
    }
    spfa();
    LL ans=inf;
    for(int i=0; i<=k; i++) ans=MIN(ans,d[n][i]);
    printf("%lld\n",ans);
    //fclose(stdin);
    //fclose(stdout);
    return 0;
}

【解析3】

90分的做法。
最后两个数据点会RE。
f [ i ] [ j ] f[i][j] f[i][j]表示用了 j j j步魔法走到 i i i,那么:
f [ i ] [ j ] = m i n ( f [ i ] [ j ] , ( k − > i 有 边 ? ( f [ k ] [ j − 1 ] − t ( k , i ) : 无 穷 大 ) ) f[i][j]=min(f[i][j],(k->i有边 ? (f[k][j-1]-t(k,i):无穷大)) f[i][j]=min(f[i][j],(k>i?(f[k][j1]t(k,i):))
t ( k , i ) t(k,i) t(k,i)是原始的边长。

【代码3】

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

#define R                  register int
#define re(i,a,b)          for(R i=a; i<=b; i++)
#define ms(i,a)            memset(a,i,sizeof(a))
#define MAX(a,b)           (((a)>(b)) ? (a):(b))
#define MIN(a,b)           (((a)<(b)) ? (a):(b))

using namespace std;

typedef long long LL;

LL const inf=1e16;
int const N=105;

int n,m,k;
LL ans=inf;
LL a[N][N],f[N][1005],d[N][N];

int main() {
    //freopen("magic.in","r",stdin);
    //freopen("magic.out","w",stdout);
    scanf("%d%d%d",&n,&m,&k);
    for(int i=1; i<=n; i++) for(int j=1; j<=n; j++)
        d[i][j]=inf;
    for(int i=1; i<=n; i++) d[i][i]=a[i][i]=0;
	for(int i=1; i<=m; i++) {
        int t,u,v;
        scanf("%d%d%d",&u,&v,&t);
        a[u][v]=d[u][v]=t;
    }
    for(int k2=1; k2<=n; k2++) for(int i=1; i<=n; i++) for(int j=1; j<=n; j++) 
        d[i][j]=MIN(d[i][j],d[i][k2]+d[k2][j]);
    for(int i=1; i<=n; i++) for(int j=1; j<=k; j++) f[i][j]=inf;
    for(int i=1; i<=n; i++) f[i][0]=d[1][i];
    for(int j=1; j<=k; j++) {
		for(int i=1; i<=n; i++) for(int k3=1; k3<=n; k3++) 
            f[i][j]=MIN(f[i][j],(a[k3][i]>0 ? f[k3][j-1]-a[k3][i] : inf));
		for(int i=1; i<=n; i++) for(int k3=1; k3<=n; k3++) 
            f[i][j]=MIN(f[i][j],f[k3][j]+d[k3][i]);
	}
    for(int i=0; i<=k; i++) ans=MIN(ans,f[n][i]);
    printf("%lld\n",ans);
	//fclose(stdin);
	//fclose(stdout);
    return 0;
}

【解析4】

100分的做法1。
解析4是对解析3的优化。
时间复杂度 O ( n 2 t + n 2 k / t ) O(n^2t+n^2k/t) O(n2t+n2k/t) t = s q r t ( k ) t = sqrt(k) t=sqrt(k),所以约等于 O ( n 2 s q r t ( k ) ) O(n^2sqrt(k)) O(n2sqrt(k)).

f [ i ] [ j ] [ k ] f[i][j][k] f[i][j][k]表示从i走到k层后的j的最小代价,按照解析3的方法dp。
因为前t层一定存在一种走法,然后发现最后只要剩下够n个魔法就一定不会停止。 t = m a x ( n , s q r t ( k ) ) t=max(n,sqrt(k)) t=max(n,sqrt(k))

【代码4】

#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
#include <algorithm>
#include <vector>

#define R                  register int
#define re(i,a,b)          for(R i=a; i<=b; i++)
#define ms(i,a)            memset(a,i,sizeof(a))
#define MAX(a,b)           (((a)>(b)) ? (a):(b))
#define MIN(a,b)           (((a)<(b)) ? (a):(b))

using namespace std;

typedef long long LL;

int const N=105;

int n,m,t,x,y,z;
LL a[N][N],d[N<<1][N<<1],ans[N][N],f[21][N][N];

int main() {
    //freopen("magic.in","r",stdin);
    //freopen("magic.out","w",stdout);
	scanf("%d%d%d",&n,&m,&t);
	memset(d,0x3f,sizeof(d));
	memset(ans,0x3f,sizeof(ans));
	memset(f,0x3f,sizeof(f));
	for(int i=1; i<=n; i++) d[i][i]=ans[i][i]=d[i+n][i+n]=0;
	d[n][n<<1]=0;
	for(int i=1; i<=m; i++){
		scanf("%d%d%d",&x,&y,&z);
		d[x][y]=d[x+n][y+n]=z;
		d[x][y+n]=-z;
	} 
	for(int i=1; i<=(n<<1); i++) for(int j=1; j<=2*n; j++) for(int k=1; k<=(n<<1); k++) 
        d[j][k]=MIN(d[j][k],d[j][i]+d[i][k]);
	for(int i=1; i<=n; i++) for(int j=1; j<=n; j++)
        f[0][i][j]=d[i][j+n];
	if(!t) {
		printf("%lld",d[1][n]);
		return 0;
	}
	if(t&1) {
		t--;
		memcpy(ans,f[0],sizeof(ans));
	}
	for(int ii=1; t; ii++) {
		for(int i=1; i<=n; i++) for(int j=1; j<=n; j++) for(int k=1; k<=n; k++)
			f[ii][i][j]=MIN(f[ii][i][j],f[ii-1][i][k]+f[ii-1][k][j]);
		if(t&(1<<ii)) {
			t-=(1<<ii);
			memcpy(a,ans,sizeof(a));
			memset(ans,0x3f,sizeof(ans));
			for(int i=1; i<=n; i++) for(int j=1; j<=n; j++) for(int k=1;k<=n;k++)
                ans[i][j]=MIN(ans[i][j],a[i][k]+f[ii][k][j]);
		}
	}
	printf("%lld\n",ans[1][n]);
	//fclose(stdin);
	//fclose(stdout);
    return 0;
}

【解析5】

100分的做法2。
矩阵乘法+快速幂。

(解析5来自 南沙客运港,原文链接,有删改)

这题思路本身其实没那么难,但是想出来真的有点难。

首先,定义任意两点之间至多使用一次魔法的最短路为 f k , i , j f_{k, i, j} fk,i,j
假设我们已经求出了任意两点之间 至多 使用一次魔法的最短路,即为 f 1 , i , j f_{1, i, j} f1,i,j
​ 后面再讲怎么求这个东西。

Q: 为什么要求这个?
A: k太大了,转移次数必须小于线性。考虑把它优化至矩阵乘法。

转移方程: f k , i , j = min ⁡ t ∈ [ 1 , n ] f k − 1 , i , t + f 1 , t , j f_{k, i, j} = \min_{t \in [1, n]} f_{k - 1, i, t} + f_{1, t, j} fk,i,j=t[1,n]minfk1,i,t+f1,t,j

人话翻译:选择一个中转点 t ,就像求最短路时一样,前面用 k - 1 次魔法,后面用 1 次魔法,这些路径当中求最小值。

Q: 为什么限定后半段只能用一次?
A: 这个转移是一个连续的过程,假如后半段用 p 次魔法,那么你完全可以在后半段转移 p 次之后再来更新全局答案。

Q: 如果我有一段路不想用魔法呢?
A: 如果允许使用魔法 ( k > 0 ),那么任意两点间最短路必须使用魔法,因为原边权都>0
还有,令 t ∈ { i , j } t \in \{i, j\} t{i,j}也就相当于没转移。

然后发现一个惊奇的事情:如果把每个fk 看成一个矩阵,那么一次转移就是f1的一次自乘,把矩阵乘法中的加号换成 m i n min min

Q: 怎么证明这个可以快速幂呢?
A: 只需证明 两个矩阵相乘,加号换成 m i n min min 这个新运算 满足结合律,即 ( A opt ⁡ B ) opt ⁡ C = A opt ⁡ ( B opt ⁡ C ) (A \operatorname{opt} B) \operatorname{opt} C = A \operatorname{opt} (B \operatorname{opt} C) (AoptB)optC=Aopt(BoptC)无论怎么结合,答案矩阵里面每一个元素都是由 原来的三个矩阵中相同的那几个元素 取 m i n min min得到的。

接下来求 f 1 , i , j f_{1, i, j} f1,i,j

f 0 , i , j f_{0, i, j} f0,i,j 肯定都会求吧,由于点数非常小,一遍 Floyd 完事。只需要管强制使用魔法的情况,两者取 m i n min min 就可以了。

强制使用魔法也很简单,枚举每一条边 (a, b)(a,b) ,强制它使用魔法,且任意两点之间都要通过这条边进行转移,取 m i n min min f 1 , i , j = min ⁡ ( f 0 , i , j , f 0 , i , a + f 0 , b , j − w i , j ) f_{1, i, j} = \min(f_{0, i, j}, f_{0, i, a} + f_{0, b, j} - w_{i, j}) f1,i,j=min(f0,i,j,f0,i,a+f0,b,jwi,j)

做完了。

代码其实不难写,一个 Floyd 板子,加上f数组的转移,再加上魔改过后的矩乘板子就搞完了。但是好多数组都要赋初值为正无穷,不要漏了,并且要特判 k = 0 的情况。

【代码5】

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

#define R                  register int
#define re(i,a,b)          for(R i=a; i<=b; i++)
#define ms(i,a)            memset(a,i,sizeof(a))
#define MAX(a,b)           (((a)>(b)) ? (a):(b))
#define MIN(a,b)           (((a)<(b)) ? (a):(b))

using namespace std;

typedef long long LL;

LL const inf=1e16;
int const N=105;
int const M=2505;

int n,m,k;
int u[M],v[M];
LL a[N][N],d[N][N];

struct matrix {
    LL m[N][N];
} f,e;

matrix multiply(matrix x,matrix y) {
    matrix c;
    for(int i=1; i<=n; i++) for(int j=1; j<=n; j++) 
        c.m[i][j]=inf;
    for(int i=1; i<=n; i++) for(int j=1; j<=n; j++) for(int k2=1; k2<=n; k2++) 
        c.m[i][j]=MIN(c.m[i][j],x.m[i][k2]+y.m[k2][j]);
    return c;
}

matrix qpower(matrix x,int y) {
    if(!y) return e;
    if(y==1) return x;
    matrix ret=qpower(x,y>>1);
    if(y&1) return multiply(multiply(ret,x),ret);
        else return multiply(ret,ret);
}

int main() {
    //freopen("magic.in","r",stdin);
    //freopen("magic.out","w",stdout);
    scanf("%d%d%d",&n,&m,&k);
    for(int i=1; i<=n; i++) for(int j=1; j<=n; j++) 
        a[i][j]=f.m[i][j]=inf;
    for(int i=1; i<=n; i++) a[i][i]=0;
    for(int i=1; i<=n; i++) e.m[i][i]=1;
	for(int i=1; i<=m; i++) {
        int t;
        scanf("%d%d%d",&u[i],&v[i],&t);
        a[u[i]][v[i]]=t;
    }
    for(int i=1; i<=n; i++) for(int j=1; j<=n; j++) 
        d[i][j]=a[i][j];
    for(int k3=1; k3<=n; k3++) for(int i=1; i<=n; i++) for(int j=1; j<=n; j++) 
        d[i][j]=MIN(d[i][j],d[i][k3]+d[k3][j]);
    if(!k) {  //特判k是否等于0
        printf("%d\n",d[1][n]);
        //fclose(stdin);
        //fclose(stdout);
        return 0;
    }
    for(int k4=1; k4<=m; k4++) {
        LL t=a[u[k4]][v[k4]];
        for(int i=1; i<=n; i++) for(int j=1; j<=n; j++) 
            f.m[i][j]=MIN(f.m[i][j],MIN(d[i][j],d[i][u[k4]]+d[v[k4]][j]-t));
    }
    matrix ans=qpower(f,k);
    printf("%lld\n",ans.m[1][n]);
	//fclose(stdin);
	//fclose(stdout);
    return 0;
}

评论 14
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值