程序员进阶之算法练习(四十二)

正文

题目一

题目链接
题目大意:
n个学生参加测试,一共有m道题,每道题的答案可能是(A, B, C, D , E)中的一个;
m道题分别有𝑎1,𝑎2,…,𝑎𝑚,共m个分数;
现在已知道n个学生对m道题目的选择,假如题目的正确答案可以任意选择,想知道所有学生最大的分数总和是多少?

输入:
第一行𝑛 and 𝑚 (1≤𝑛,𝑚≤1000)
接下来n行,每行有m个字符,每个字符是 (A, B, C, D or E)表示选择的答案;
接下来一行,有m个整数,𝑎1,𝑎2,…,𝑎𝑚 (1≤𝑎𝑖≤1000)

输出:
最大的分数。

Examples
input
2 4
ABCD
ABCE
1 2 3 4
output
16

样例解释:答案是ABCD时,分数最大,最大值是16

题目解析:
每道题是相对独立的,可以分来开看;
对于每一道题,选择答案人数最多的结果,乘以分数即可得到这道题的最大分数;
累加每一道题得到 最大分数和。

    int n, m;
    cin >> n >> m;
    
    string str[1001];
    for (int i = 0; i < n; ++i) {
        cin >> str[i];
    }
    int a[1001], ans = 0;
    for (int i = 0; i < m; ++i) {
        cin >> a[i];
        int v[5] = {0}, cnt = 0;
        for (int j = 0; j < n; ++j) {
            v[str[j][i] - 'A']++;
            cnt = max(cnt, v[str[j][i] - 'A']);
        }
        ans += cnt * a[i];
    }
    
    cout << ans << endl;

题目二

题目链接
题目大意:
有n个数字,有一个操作:选择两个数字a[i]和a[j](i ≠ j),使得a[i]=a[i]-1,a[j]=a[j]-1;
现在想知道,能否执行若干次操作,使得所有的数字变为0.

输入:
第一行,整数𝑛 (2≤𝑛≤10e5)
第二行,n个整数𝑎1,𝑎2,…,𝑎𝑛 (1≤𝑎𝑖≤10e9)

输出:
如果可以,输出YES;
如果不可以,输出NO;

Examples
input
4
1 1 2 2
output
YES
input
6
1 2 3 4 5 6
output
NO

题目解析:
这里的核心逻辑是如何分配这些数字。
先从小范围数据来分析,假如n=2,那么没得选只能a[0],a[1]操作;
假如n=3,我们先把数组从小到大排序,有a[0]<a[1]<a[2];
容易知道,如果a[0]+a[1]<a[2]的话,是无解的;
同时如果a[0]+a[1]+a[2]=sum,sum%2==1的话,也是无解的;
在其他情况下,有a[0]+a[1]>=a[2],并且(a[0]+a[1]-a[2])%2==0;
那么,可以对a[0],a[1]进行操作,则相当于(a[0]+a[1]) -= 2,最终可以得到a[0]+a[1]=a[2];

当n=4的时候,同样先排序,得到a[0]<a[1]<a[2]<a[3];
为了复用上面的思考过程,我们可以把a[0],a[1]合并成一个数字来看,这个数字除了可以和其他数字进行-1操作,也可以自己和自己进行-1的操作;
再重复上面的思考过程,最终发现只有两种情况无解:
1、sum%2==1,和为奇数无解;
2、a[n-1]*2>sum,最大数超过和的一半,无解;
其他的情况,都可以用上面类似的方法得到一个解。

    int n;
    cin >> n;
    lld sum = 0;
    for (int i = 0; i < n; ++i) {
        cin >> a[i];
        sum = sum + a[i];
    }
    sort(a, a + n);
    if (sum % 2 || 2 * a[n - 1] > sum) {
        cout << "NO" << endl;
    }
    else {
        cout << "YES" << endl;
    }
    

题目三

题目链接
题目大意:
有n个数字,𝑎1,𝑎2,…,𝑎𝑛;
现在可以进行k次操作,每次可以选择一个数字a[i],使得a[i]=a[i]+1;
问,进行k次操作,数组中的中位数最大为多少?

输入:
第一行,𝑛 and 𝑘 (1≤𝑛≤2⋅1e5, 𝑛是奇数, 1≤𝑘≤1e9)
第二行,𝑎1,𝑎2,…,𝑎𝑛 (1≤𝑎𝑖≤1e9).

输出:
最大的中位数。

Examples
input
3 2
1 3 5
output
5
input
5 5
1 2 1 1 1
output
3

题目解析:
题目的数据没有先后顺序相关,可以先对数据排个序,方便处理。
题目要求是中位数最大,假设我们的目标是s,那么从a[(n-1)/2]开始,所有的数字小于s的都要变大;
因为k值比较大,模拟的做法不可取。
考虑到在变大的过程中,我们每次都是固定处理后面n/2个数,那么如果s有解,则s-1也一定有解;(因为可以少变一些数字)
基于此线性特点,可以用二分来解决。

注意这里有个trick,二分的范围。

比如说这样的数据:

 1 1000000000
 1000000000

另外就是二分的时候,要注意用int64来处理。

bool check(lld mid, lld n, lld k) {
    for (lld i = (n - 1) / 2; i < n; ++i) {
        if (a[i] < mid && k >= 0) {
            k -= mid - a[i];
        }
    }
    return k >= 0;
}

int main(int argc, const char * argv[]) {
    // insert code here...
    lld n, k;
    cin >> n >> k;
    for (lld i = 0; i < n; ++i) {
        cin >> a[i];
    }
    sort(a, a + n);
    
    lld left = 1, right = a[n - 1] + k, ans = left;
    while (left <= right) {
        lld mid = (left + right) / 2;
        if (check(mid, n, k)) {
            ans = mid;
            left = mid + 1;
        }
        else {
            right = mid - 1;
        }
    }
    cout << ans << endl;
    
    return 0;
}

题目四

题目链接
题目大意:
有一个n x m的网格,小明站在位置(1,1);
现在小明每次可以选择移动一步:向上、向左、向右,但是不能向下,也不能超过网格;
网格中,只有某些特定的列可以往上走,这些列有q个(𝑏1,𝑏2,…,𝑏𝑞)
某些格子放着宝藏,这些格子有k个,分别是𝑟𝑖,𝑐𝑖, (1≤𝑟𝑖≤𝑛, 1≤𝑐𝑖≤𝑚)

现在想知道,小明拿到所有的宝物,最少需要多少步;

输入:
第一行, 𝑛, 𝑚, 𝑘 and 𝑞 (2≤𝑛,𝑚,𝑘,𝑞≤2⋅1e5, 𝑞≤𝑚)
接下来k行,每行2个整数 𝑟𝑖,𝑐𝑖, (1≤𝑟𝑖≤𝑛, 1≤𝑐𝑖≤𝑚)
最后一行有q个整数,𝑏1,𝑏2,…,𝑏𝑞 (1≤𝑏𝑖≤𝑚)

输出:
最少移动步数。

Examples
input
3 3 3 2
1 1
2 1
3 1
2 3
output
6
input
3 5 3 2
1 2
2 3
3 1
1 5
output
8
input
3 6 3 2
1 6
2 2
3 4
1 6
output
15

题目解析:
理清题目的要求和限制之后,我们可以知道:
1、我们要拿到所有的宝藏,就需要把每一层宝物都拿完才能选择上一层;
2、选择从哪一列上去是一个决策点;
3、假如在第i行时,初始位置是在第x列,计划选择第y列上去,则这一层的遍历最优解已经决定;

接下来简化这些问题,先看某一层的遍历宝藏问题,从贪心的角度来看,我们只有两个选择:
1、先走到最左边的宝藏,再走到最右边的宝藏;
2、先走到最右边的宝藏,再走到最左边的宝藏;
那么以这两个状态为节点,我们有dp[i][0],dp[i][1]表示第i行,站在最左边和最右边的最小步数;
同时,我们需要把这一层的宝藏拿完,于是有dp[i][2],dp[i][3]表示第i行拿完宝藏后,站在最左边和最右边的最小步数;

容易知道,转移过程有两个:
a.dp[i][0],dp[i][1]可以由dp[i-1][2],dp[i-1][3]转移过来;(表示拿完宝藏上来)
b.dp[i][2],dp[i][3]可以由dp[i][0],dp[i][1]转移过来;(表示拿完这一层的宝藏)

情况b很简单,直接计算距离就可以得到转移方程;
情况a比较复杂,有两个转移初始点,同时有m个选择上来的地点,如果每个都遍历一遍,转移的时间代价太高;
为了简化这个过程,可以选出起点左右最近的两个点、终点左右最近的两个点,这样复杂度就从O(M)降为O(1);
这个查找过程,可以用二分来解决。

注意事项:
错误1:第一行为空的情况,插入字符0;
错误2:某一行为空的情况,累计lastRow;
错误3:dp[i-1]改为lastRow;
错误4:long long;
错误5:binary search 的时候,传入的n,应该是q;
错误6:初始化错误,写成int的最大值,应该是longlong最大值;

写的时候没有专心,分了几次写,问题也多好几个;


typedef long long lld;
const lld N = 201000, M = 3010100, inf = 0x7fffffff;
const lld llinf = 0x7fffffff7fffffffll;

vector<lld> a[N];
lld dp[N][4];
lld b[N];

pair<lld, lld> binarySearch(lld val[], lld n, lld k) {
    pair<lld, lld> pos;
    lld left = 0, right = n; // [left, right) 左开右闭
    pos.first = left;
    while (left < right) {
        lld mid = (left + right) / 2;
        if (val[mid] == k) {
            pos.first = mid;
            break;
        }
        else if (val[mid] < k) {
            pos.first = mid;
            left = mid + 1;
        }
        else {
            right = mid;
        }
    }
    
    left = 0, right = n, pos.second = n - 1;
    while (left < right) {
        lld mid = (left + right) / 2;
        if (val[mid] == k) {
            pos.second = mid;
            break;
        }
        else if (val[mid] < k) {
            left = mid + 1;
        }
        else {
            pos.second = mid;
            right = mid;
        }
    }
    return make_pair(val[pos.first], val[pos.second]);
}

lld cost(lld src, pair<lld, lld> pass, lld dest) {
    return min(abs(src - pass.first) + abs(pass.first - dest), abs(src - pass.second) + abs(pass.second - dest));
}


int main(int argc, const char * argv[]) {
    // insert code here...
    lld n, m, k, q;
    cin >> n >> m >> k >> q;
    lld maxRow = 0;
    for (lld i = 0; i < k; ++i) {
        lld x, y;
        cin >> x >> y;
        --x;--y;
        a[x].push_back(y);
        maxRow = max(maxRow, x);
    }
    for (lld i = 0; i < q; ++i) {
        cin >> b[i];
        --b[i];
    }
    sort(b, b + q);
    if (a[0].size() == 0) {
        a[0].push_back(0); // 避免第一层没有宝藏的情况
    }
    for (lld i = 0; i < n; ++i) {
        sort(a[i].begin(), a[i].end());
    }
    
    dp[0][0] = a[0].front();
    dp[0][1] = a[0].back();
    dp[0][2] = dp[0][1] + abs(a[0].back() - a[0].front());
    dp[0][3] = dp[0][0] + abs(a[0].back() - a[0].front());
    lld lastRow = 0;
    for (lld i = 1; i < n; ++i) {
        if (a[i].size()) {
            // dp[i][0]最左边,可以从下一层的dp[i-1][2]最左边,dp[i-1][3]最右边转移过来
            pair<lld, lld> posLeft = binarySearch(b, q, a[lastRow].front());
            pair<lld, lld> posRight = binarySearch(b, q, a[lastRow].back());
            
            dp[i][0] = llinf; // init
            // 从下一层的最左边上来
            dp[i][0] = min(dp[i][0], dp[lastRow][2] + (i-lastRow) + cost(a[lastRow].front(), posLeft, a[i].front()));
            // 从下一层的最右边上来
            dp[i][0] = min(dp[i][0], dp[lastRow][3] + (i-lastRow) + cost(a[lastRow].back(), posRight, a[i].front()));
            
            dp[i][1] = llinf; // init
            // 从下一层的最左边上来
            dp[i][1] = min(dp[i][1], dp[lastRow][2] + (i-lastRow) + cost(a[lastRow].front(), posLeft, a[i].back()));
            // 从下一层的最右边上来
            dp[i][1] = min(dp[i][1], dp[lastRow][3] + (i-lastRow) + cost(a[lastRow].back(), posRight, a[i].back()));
            
            dp[i][2] = dp[i][1] + abs(a[i].front() - a[i].back());
            dp[i][3] = dp[i][0] + abs(a[i].front() - a[i].back());
            lastRow = i;
        }
    }

    
    cout << min(dp[maxRow][2], dp[maxRow][3]) << endl;
    
    return 0;
}

总结

四个题目均来自于 Codeforces Round #577 (Div. 2)
前三题偏思考,代码量比较小;
第四题的代码量看似比较多,其实就是动态规划+二分,只是代码写得比较拖沓:因为在尝试按照工程的思想去做题。
先做规划,想好方案,拆分模块,按部就班。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269