【解题报告】BestCoder Round #75

本文是BestCoder Round #75的解题报告,涵盖了A至E五道题目。A题通过辗转相除法解决King's Cake问题;B题利用预处理判断密码合法性;C题使用动态规划求解King's Order,一维DP即可;D题以约瑟夫环变形问题为基础,建立固定策略的递推关系;E题通过费用流模型解决飞行员调度问题。
摘要由CSDN通过智能技术生成

题目链接


A. King’s Cake(HDU5640)

思路

不妨假设 n>m ,那么国王会一直切下 m×m 大小的蛋糕,直到 nm 为止,然后重复切下 n×n 大小的蛋糕直到 nm ……最后当 n=m 时国王就得到了最后一个蛋糕。实现这个过程只需模拟这个过程让两个数反复相减即可。另外,用辗转相除法可以加速这个过程。

代码
#include <cstdio>

int t, n, m;

int gcd(int a, int b) {
    return b ? a / b + gcd(b, a % b) : 0;
}

int main() {
    scanf("%d", &t);
    while(t--) {
        scanf("%d%d", &n, &m);
        printf("%d\n", gcd(n, m));
    }
    return 0;
}

B.King’s Phone(HDU5641)

思路

已知满足要求的密码有三个条件:

  1. 密码的长度至少是4
  2. 每个数字只能出现一次
  3. 经过密码的两个数字时不能路过没访问过的数字

那么针对这三个条件相应地进行三种判断:

  1. 直接判断密码长度是否满足条件
  2. 用vis[]数组记录该密码是否已经出现
  3. 预处理m[][]数组,m[i][j] = k表示键盘上i,j数字之间隔着k,那么可以遍历密码,若储存密码的数组为a,则m[a[i]][a[i-1]]无对应的k,或k已经出现过则满足条件
代码
#include <cstdio>
#include <cstring>

bool vis[10];
int t, k, a[10], m[10][10];

void init() {
    memset(m, 0, sizeof(m));
    for(int i = 1; i < 10; i++) {
        for(int j = 1; j < 10; j++) {
            if(i + j == 10) {
                m[i][j] = 5;
            }
        }
    }
    m[1][3] = m[3][1] = 2; m[7][9] = m[9][7] = 8;
    m[1][7] = m[7][1] = 4; m[3][9] = m[9][3] = 6;
}

bool judge() {
    if(k < 4) return false;
    memset(vis, 0, sizeof(vis));
    for(int i = 0; i < k; i++) {
        if(a[i] < 1 || a[i] > 9 || vis[a[i]]) {
            return false;
        }
        vis[a[i]] = true;
        if(i == 0) continue;
        int mid = m[a[i]][a[i-1]];
        if(mid && vis[mid] == false) {
            return false;
        }
    }
    return true;
}

int main() {
    init();
    scanf("%d", &t);
    while(t--) {
        scanf("%d", &k);
        for(int i = 0; i < k; i++) {
            scanf("%d", &a[i]);
        }
        puts(judge() ? "valid" : "invalid");
    }
}

C.King’s Order(HDU5642)

思路

读题的时候就感觉这题可以利用最优子结构,因此应该是个动态规划的题目。然后觉得应该是通过计算不符合条件的情况来间接得到结果。但是这种思路似乎是不太好找到转移方程的。于是还是用直接的方法来解决。假设d[i]表示长度为i的合法字符串的数量,那么考虑是否可以利用 d[i1] 来得到 d[i] 。对于长度为 i 的字符串的 i1 长度的前缀,它的所有合法的情况下,在第 i 个字符的位置填 26 个小写字母应该都是可以的,除了末尾 3 个字符相同的情况,这种情况下就只有 25 种小写字母可以填了。那么可不可以知道末尾 3 个字符相同的合法的情况呢?应该是可以的,于是尝试增加维数来携带更多的信息。假设 d[i][1]d[i][2] d[i][3] 分别是字符串长度为 i 时,以 1,2 3 个相同字符结尾的合法情况。那么很自然地, d[i][2]=d[i1][1]d[i][3]=d[i1][2] (在末尾添加相同的字符)。而且无论之前的字符串以多少个相同字符结尾,总能够找到一个与当前末尾字符不同的字符添加在末尾之后,于是有 d[i][1]=d[i1][1]+d[i1][2]+d[i1][3] ,当然,要记得取模。最后的答案就是 (d[n][1]+d[n][2]+d[n][3]) % m

代码

#include <cstdio>

const int maxn = 2e3 + 5, mod = 1e9 + 7;
int t, n;
long long d[maxn][4];

int main() {
    d[1][1] = 26;
    for(int i = 2; i < maxn; i++) {
        d[i][1] = 25 * (d[i-1][1] + d[i-1][2] + d[i-1][3]) % mod;
        d[i][2] = d[i-1][1];
        d[i][3] = d[i-1][2];
    }
    scanf("%d", &t);
    while(t--) {
        scanf("%d", &n);
        printf("%I64d\n", (d[n][1] + d[n][2] + d[n][3]) % mod);
    }
    return 0;
}

思路

后来才知道,一维的动态规划已经足以表示解题需要的所有信息。我们可以将状态转移方程写成这样 d[i]=26×d[i1]x ,其中x是长度为i的最后四个字符相等的字符串数。因为最后四个字符相等了,因此将 d[i4] 乘上 25 (代表与末尾不相同的字符)就可以得到 x 。另外依然不要忘记取模,特别是在有减号的式子里,要将被减数与减数分别取模,然后加上模数,再将这个结果取模。

代码

#include <cstdio>

const int maxn = 2e3 + 5, mod = 1e9 + 7;
int t, n;
long long d[maxn][4];

int main() {
    d[1][1] = 26;
    for(int i = 2; i < maxn; i++) {
        d[i][1] = 25 * (d[i-1][1] + d[i-1][2] + d[i-1][3]) % mod;
        d[i][2] = d[i-1][1];
        d[i][3] = d[i-1][2];
    }
    scanf("%d", &t);
    while(t--) {
        scanf("%d", &n);
        printf("%I64d\n", (d[n][1] + d[n][2] + d[n][3]) % mod);
    }
    return 0;
}

D.King’s Game(HDU5643)

思路

这是约瑟夫环问题的变形。如果用链表来做模拟的话,每组的复杂度都将是 O(1+2+...+n)=O(n2) ,而 O(T×n2) 的总复杂度是我们无法忍受的。事实上,每组只允许我们用 O(n) 复杂度去解决。假设现在已知某个 n ,我们在草稿纸上画一个编号从 0 n1 的圈(这样编号是为了待会儿方便取模)。手动模拟一下不难发现,其实每次的策略是固定的,就是会有人出局,这十分的麻烦。因为每次策略都是固定的,所以我们尝试建立在已知 n=6 的情况下, f(6) f(5) 的关系(其中 f(x) 表示还有 x 个人剩下,以当前位置为编号 0 时最终的幸存者的编号)。当还有 5 个人剩下时,原始编号为 1 的那个人开始报数,所以他的新编号为 0 ,当报数到 f(5) 时,新编号为 f(5) 的那个人的原始编号实际上是 1+f(5) ,由于 1+f(5) 可能会超过 6 因此这个式子要对 6 取模: (1+f(5)) 。到这里,我们终于成功建立了 f(6) f(5) 的关系: f(6)=(f(5)+1) % 6 。推广到一般的情况就应该是: f(i)=(f(i1)+k(i)) ,其中 k(i) 表示剩下 i 个人时要报到哪个数才有人出局。

代码

#include <cstdio>

int t, n, f;

int main() {
    scanf("%d", &t);
    while(t--) {
        scanf("%d", &n);
        f = 0;
        for(int i = 2; i <= n; i++) {
            f = (f + n - i + 1) % i;
        }
        printf("%d\n", f + 1);
    }
    return 0;
}

E.King’s Pilots(HDU5644)

思路

读题以后发现这是个最优化问题。并且与解相关的量太多,似乎用贪心,搜索和动态规划都不好办。但发现题目中的条件有点“供需关系”的意思。于是考虑是否能用网络流(这里应该是费用流)来解决。
先设置 n 个点(编号从 n+1 n+n ,原因稍后解释)表示第n天。首先从源点(编号为 0 )引一条容量和费用为 (k,0) 边到点 1 ,然后对每个点 i 连一条到 i+1 的边 (INF,0) 。这两种边表示初始的时候有 k 个飞行员,使用他们是免费的。现在考虑飞行员的训练制度。首先从源点引一条到 P 点的边 (INF,Q) ,表示第 P 天就可以使用训练好的飞行员了,这些飞行员的供应是无限的,但每位飞行员要付 Q 单位的钱。最后考虑飞行员的休假制度。对每个点 i 连一条边 (p[i],0) 到汇点 t ,表示每天都有 p[i] 位飞行员要去休假。那么休假的飞行员回来的关系该如何表示呢?如果直接从源点向表示“第 i 天”的点引边的话,只能表示 1 种休假制度,但是我们有m种休假制度。所以显然不能直接这样引边。那么我们能不能先把飞行员囤在某些点上,然后依照不同的休假制度,用不同的方法从这些点向第 i 天供应呢?显然是可以的。我们将第 i 天的点拆成 xi (编号从 1 n )和 yi (编号从 n+1 n+n ) , yi 保持原来“第 i 天的点”的连边方式, xi 要重新连不同的边(这就是为什么之前要把表示第 i 天的点从 n+1 编号到 n+n )。从源点向 xi 引边 (p[i],0) ,表示每天都有 p[i] 个飞行员去休假。然后从 xi yi+t[j] 引边 (INF,s[j]) ,表示对第 j 种休假方案,过了 t[j] 天后花 s[j] 元钱就能将休假结束的飞行员请回来。
这样,图就建好了。对这个图运行一次最小费用最大流。当最大流等于 i=1np[i] 的时候最小费用就是解。否则无解。

代码

#include <cstdio>
#include <cstring>
#include <queue>
#include <vector>
#include <algorithm>
using namespace std;

const int maxn = 405, maxm = 10, INF = 1e9;
int cs, n, k, m, P, Q, S, T, sum, ans, p[maxn], s[maxm], t[maxm];

// 存储边的信息的结构体
struct edge {
    int from, to, cap, flow, cost;
    edge(int from, int to, int cap, int flow, int cost): 
        from(from), to(to), cap(cap), flow(flow), cost(cost) {}
};

// 最小费用最大流模板
struct MCMF {
    int v;
    vector <edge> edges;
    vector <int> G[maxn];
    int inq[maxn];          // 是否在队列中
    int d[maxn];            // Bellman-Ford
    int p[maxn];            // 上一条弧
    int a[maxn];            // 可改进量
    void init(int v) {
        this->v = v;
        for(int i = 0; i < v; i++) {
            G[i].clear();
        }
        edges.clear();
    }
    // 往邻接表中添加一条边
    void addEdge(int from, int to, int cap, int cost) {
        G[from].push_back(edges.size());
        edges.push_back(edge(from, to, cap, 0, cost));
        G[to].push_back(edges.size());
        edges.push_back(edge(to, from, 0, 0, -cost));
    }
    // 用Bellman-Ford算法寻找最短增广路
    bool BellmanFord(int s, int t, int& flow, int& cost) {
        for(int i = 0; i < v; i++) {
            d[i] = INF;
        }
        memset(inq, 0, sizeof(inq));
        d[s] = 0;
        inq[s] = 1;
        p[s] = 0;
        a[s] = INF;
        queue <int> q;
        q.push(s);
        while(!q.empty()) {
            int u = q.front();
            q.pop();
            inq[u] = 0;
            for(int i = 0; i < G[u].size(); i++) {
                edge& e = edges[G[u][i]];
                if(e.cap > e.flow && d[e.to] > d[u] + e.cost) {
                    d[e.to] = d[u] + e.cost;
                    p[e.to] = G[u][i];
                    a[e.to] = min(a[u], e.cap - e.flow);
                    if(!inq[e.to]) {
                        q.push(e.to);
                        inq[e.to] = 1;
                    }
                }
            }
        }
        if(d[t] == INF) return false; // s-t不连通,失败退出
        flow += a[t];
        cost += d[t] * a[t];
        for(int u = t; u != s; u = edges[p[u]].from) {
            edges[p[u]].flow += a[t];
            edges[p[u]^1].flow -= a[t];
        }
        return true;
    }
    // 解出最小费用和最大流
    int Mincost(int s, int t) {
        int flow = 0, cost = 0;
        while(BellmanFord(s, t, flow, cost));
        if(flow == sum) return cost;
        else return -1;
    }
}o;

int main() {
    scanf("%d", &cs);
    while(cs--) {
        scanf("%d%d", &n, &k);
        sum = 0;
        for(int i = 1; i <= n; i++) {
            scanf("%d", &p[i]);
            sum += p[i];
        }
        scanf("%d%d%d", &m, &P, &Q);
        for(int i = 1; i <= m; i++) {
            scanf("%d%d", &s[i], &t[i]);
        }
        S = 0, T = 2 * n + 1;
        // 初始化邻接表
        o.init(2 * n + 2);
        // 供应初始的免费飞行员
        o.addEdge(S, n + 1, k, 0);
        // 供应训练好的飞行员
        if(P <= n) {
            o.addEdge(S, n + P, INF, Q);
        }
        // 存储休假回来的飞行员
        for(int i = 1; i <= n; i++) {
            o.addEdge(S, i, p[i], 0);
        }
        // 供应休假回来的飞行员
        for(int i = 1; i <= m; i++) {
            for(int j = 1; j + t[i] <= n; j++) {
                o.addEdge(j, n + j + t[i], INF, s[i]);
            }
        }
        // 使用前几天未使用的飞行员
        for(int i = 1; i < n; i++) {
            o.addEdge(n + i, n + i + 1, INF, 0);
        }
        // 飞行员去休假
        for(int i = 1; i <= n; i++) {
            o.addEdge(n + i, T, p[i], 0);
        }
        // 判断是否有解并输出
        ans = o.Mincost(S, T);
        if(ans < 0) {
            puts("No solution");
        }
        else printf("%d\n", ans);
    }
    return 0;
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
经导师精心指导并认可、获 98 分的毕业设计项目!【项目资源】:微信小程序。【项目说明】:聚焦计算机相关专业毕设及实战操练,可作课程设计与期末大作业,含全部源码,能直用于毕设,经严格调试,运行有保障!【项目服务】:有任何使用上的问题,欢迎随时与博主沟通,博主会及时解答。 经导师精心指导并认可、获 98 分的毕业设计项目!【项目资源】:微信小程序。【项目说明】:聚焦计算机相关专业毕设及实战操练,可作课程设计与期末大作业,含全部源码,能直用于毕设,经严格调试,运行有保障!【项目服务】:有任何使用上的问题,欢迎随时与博主沟通,博主会及时解答。 经导师精心指导并认可、获 98 分的毕业设计项目!【项目资源】:微信小程序。【项目说明】:聚焦计算机相关专业毕设及实战操练,可作课程设计与期末大作业,含全部源码,能直用于毕设,经严格调试,运行有保障!【项目服务】:有任何使用上的问题,欢迎随时与博主沟通,博主会及时解答。 经导师精心指导并认可、获 98 分的毕业设计项目!【项目资源】:微信小程序。【项目说明】:聚焦计算机相关专业毕设及实战操练,可作课程设计与期末大作业,含全部源码,能直用于毕设,经严格调试,运行有保障!【项目服务】:有任何使用上的问题,欢迎随时与博主沟通,博主会及时解答。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值