程序员进阶之算法练习(五十三)

2023-09-25 18:40:12 浏览数 (2)

正文

题目1

题目链接 题目大意: 有三堆石头,分别有a、b、c个; 现在可以执行操作: 1、从第一堆拿出1个石头,第二堆拿出2个石头; 2、从第二堆拿出1个石头,第三堆拿出2个石头; 以上的操作,必须保证堆中有足够石头才允许操作。

问最多,可以从这三堆石头中拿出多少个。

输入: 第一行,是样例个数 ? (1≤?≤100); 接下来t行表示t个样例,每个样例一行,每行有三个数字a、b、c (0≤?,?,?≤100);

输出: 每个样例一行,输出拿到的石头数量。

input 3 3 4 5 1 0 5 5 3 2

output 9 0 6

题目解析: 由题目的条件可以知道,操作1和操作2都会依赖第二堆的石头,所以第二堆石头的分配就成为了关键; 为了高效利用第二堆石头,我们优先执行操作2,再执行操作1。

代码语言:javascript复制
int main(int argc, const char * argv[]) {
    int a, b, c;
    int t;
    cin >> t;
    while (t--) {
        cin >> a >> b >> c;
        int ans = min(c / 2, b);
        cout << min((b - ans) / 2, a) * 3   ans * 3 << endl;
    }
    return 0;
}
题目 2

题目链接 题目大意: 给出两个整数l和r,已知l <= r; 现在希望找到一个整数x,要求是 l<=x<=r,并且x中没有相同的数字; 如果能找到则输出这个数字; 如果不能找到则输出-1;

输入: 两个整数l和r; (1≤?≤?≤1e5)

输出: 输出整数x;如果不存在则输出-1;

题目解析: 直接遍历整数区间的数字,判断每个数字是否合法。

扩展思考题?:假如题目的数据范围非常大,甚至没有限制呢?在无法枚举的情况下,求出一个最小的解。

代码语言:javascript复制
int main(int argc, const char * argv[]) {
    int x, y;
    cin >> x >> y;
    while (x <= y) {
        int ok = 1;
        int a[10] = {0};
        int tmp = x;
        while (tmp) {
            if (a[tmp % 10]) ok = 0;
            a[tmp] = 1;
            tmp /= 10;
        }
        if (ok) {
            cout << x << endl;
            return 0;
        }
          x;
    }
    cout << -1 << endl;
    
    return 0;
}
题目3

题目链接 题目大意: 有n个整数,?1,?2,…,??; 可以去掉中间某一段连续的数字,要求剩下的数字没有重复。 问,最少需要去掉多少个数字。

输入: 第一行,整数? (1≤?≤2000) 第二行,n个整数?1,?2,…,?? (1≤??≤1e9)

输出: 最少需要去掉数字个数

Examples input 3 1 2 3 output 0 input 4 1 1 2 2 output 2

题目解析: 不考虑复杂度,直接枚举去掉区间的坐标[l, r],有n^2的可能; 判断剩下数字有没有重复需要O(N)的时间,总的时间复杂度是O^3,超过了可接受范围;

假如去掉x个数字有解,则去掉x 1个数字也有解,可以用2分来优化; 这样复杂度可以优化为O(NNlogN),可接受范围内。

提交错误(TLE)1次,数字较大且map耗时较多;改为scanf,添加hash处理,可以满足要求。

代码语言:javascript复制
    map<int, int> h;
    for (int i = 0; i < n;   i) {
        int t;
        scanf("%d", &t);
        if (!h[t]) {
            h[t] =   cnt;
        }
        a[i] = h[t];
    }

    int left = 0, right = n; // [left, right)
    int ans = n - 1;
    while (left < right) {
        int mid = (left   right) / 2;
        int ok = 0;
        for (int k = 0; k < n;   k) { //
            if (check(n, k, k   mid)) {
                ok = 1;
                break;
            }
        }
        if (ok) {
            ans = mid;
            right = mid;
        }
        else {
            left = mid   1;
        }
    }
    cout << ans << endl;

更优化的做法: 假设去掉的数字区间是[l, r],那么剩下的就是[0, l-1]和[r 1, n-1]的区间。 容易知道[0, l-1]和[r 1, n-1]里面是没有重复元素的。 从0开始遍历,可以很容易得到以0为起点的不重复元素区间; 同理,从n-1开始向左遍历,可以得到以n-1为结束的不重复元素区间; 以上两步的处理都是O(N)复杂度。

假设得到左边最大的不重复元素区间,也得到右边最大不重复元素区间; 那么就可以枚举左边区间长度(从长度为0开始),对于[0, k)的区间,求出[0, k)中所有元素的最右边距离difMax,那么从difMax 1开始,右边的元素就不会再和左边的[0, k)相同,这样再和右边最大不重复元素区间取一个较小值。

可以O(N)解决这个问题。

提交错误(WA)1次,少考虑了左边完全不需要的情况,ans的初始化有误,比如说: 8 1 1 1 1 5 6 7 1 这样的样例会返回7,因为默认了第一个1数字会选择,实际上还有一种情况是左边数字1区间不取。

代码语言:javascript复制
int a[N];
int vis[N];
int cnt = 0;
// [l, r)
bool check(int n, int l, int r) {
    memset(vis, 0, sizeof(vis));
    for (int i = 0; i < l;   i) {
        if (!vis[a[i]]) {
            vis[a[i]] = 1;
        }
        else {
            return false;
        }
    }
    for (int i = r; i < n;   i) {
        if (!vis[a[i]]) {
            vis[a[i]] = 1;
        }
        else {
            return false;
        }
    }
    return true;
}

int main(int argc, const char * argv[]) {
    // insert code here...
    int n;
    cin >> n;
    
    map<int, int> leftMap, rightMap, gapMap;
    for (int i = 0; i < n;   i) {
        scanf("%d", &a[i]);
        gapMap[a[i]] = i;
    }
    
    int leftDifMax = 0, rightDifMax = 0;
    for (int i = 0; i < n;   i) {
        if (leftMap[a[i]]) {
            break;
        }
        leftMap[a[i]] = 1;
        leftDifMax = i;
    }
    
    for (int i = n - 1; i >= 0; --i) {
        if (rightMap[a[i]]) {
            break;
        }
        rightMap[a[i]] = 1;
        rightDifMax = i;
    }
    
    int tmp = 0, ans = (n - rightDifMax);
    for (int i = 0; i <= leftDifMax;   i) {
        tmp = max(tmp, gapMap[a[i]]);
        int r = max(rightDifMax, tmp   1);
        ans = max(ans, i   1   (n - r));
    }
    
    cout << (n - ans) << endl;
       
    return 0;
}
题目4

题目链接 题目大意: 给出n个数字,每次可以选择若干个数字,将他们进行平均;(如果不能整除,会用分数来表示) 这个操作的次数不限制;

问最终最多有多少个数字可以大于x?

输入: 第一行,整数?表示有t个样例数量 (1≤?≤1000) 每个样例第一行, 整数? and ? (1≤?≤10^5, 1≤?≤10^9) 接下来一行n个整数?1,?2,…,?? (1≤??≤10^9)

输出: 每个样例一行,最终大于数字x的数量;

Examples input 4 4 3 5 1 2 1 4 10 11 9 11 9 2 5 4 3 3 7 9 4 9

⁣output ⁣2 4 0 3

题目解析: 把数字从大到小排序,从数字大的开始选择: 1、如果该数字大于x,直接入选; 2、如果该数字小于x,并且加进来不会导致sum/count 小于x,则加进来;

直到数字不能选择,则得到最多的数字。

代码语言:javascript复制
int main(int argc, const char * argv[]) {
    // insert code here...
    
    int t;
    cin >> t;
    while (t--) {
        lld n, x;
        cin >> n >> x;
        
        for (int i = 0; i < n;   i) {
            scanf("%d", &a[i]);
        }
        sort(a, a   n);
        lld sum = 0, ans = 0;
        for (int i = n - 1; i >= 0; --i) {
            if (sum   a[i] >= x * (ans   1)) {
                sum  = a[i];
                  ans;
            }
            else {
                break;
            }
        }
        cout << ans << endl;
    }
    
    return 0;
}
题目5

题目链接 题目大意: 小明在打游戏,有n只怪物围绕成一圈,第1只和第n只相邻; 第i只怪物的血量为a[i],小明每次可以攻击一只怪物,使得怪物的血量-1; 当怪物的血量小于等于0的时候会死亡,并且产生爆炸,对第i 1个位置的怪物造成b[i]点伤害;(如果是第n只怪物爆炸,则是对第1只怪物造出伤害) 现在想知道,小明最少攻击多少次,才能使得所有怪物都爆炸;

输入: 第一行,整数?表示有t个样例数量 (1≤?≤1000) 每个样例第一行, 整数? (2≤?≤300000) 接下来n行,每行两个整数?? and ?? (1≤??,??≤1e12)

输出: 每个样例一行,小明的最少攻击次数。

Examples input 1 3 7 15 2 14 5 3 ⁣output ⁣6

题目解析: 题目有两个点使得决策比较复杂:1、n个怪物绕成一个圈;2、怪物死亡之后,会产生爆炸; 先不考虑怪物死亡后爆炸的问题,就是n只怪物的血量和; 再考虑爆炸的情况,由于题目要求的是最终所有怪物都死亡,即所有怪物都会爆炸,那么可以直接减去爆炸影响的血量;

接下来的问题是,找到n个怪物中,应该首先攻击哪一个,使得其最先爆炸; 这个可以先统计一遍所有怪物减去被爆炸影响后的血量和,然后遍历每一个怪物,枚举这个怪物如果最先爆炸,最终的攻击次数。

代码语言:javascript复制
int main(int argc, const char * argv[]) {
    // insert code here...
    
    int t;
    cin >> t;
    while (t--) {
        int n;
        cin >> n;
        
        for (int i = 0; i < n;   i) {
            scanf("%lld%lld", &a[i], &val[i]);
        }
        
        lld sum = max(0ll, a[0] - val[n - 1]);
        for (int i = 1; i < n;   i) {
            sum = sum   max(0ll, a[i] - val[i - 1]);
        }
        
        lld ans = sum - max(0ll, a[0] - val[n - 1])   a[0]; // 直接打死第0个
        for (int i = 1; i < n;   i) {
            lld tmp = sum - max(0ll, a[i] - val[i - 1])   a[i];
            ans = min(ans, tmp);
        }

        cout << ans << endl;
    }   
    
    return 0;
}

思考?: 如果是双向爆炸呢? 即位置i有多种可能爆炸顺序, i最先爆炸,i-1先爆炸然后i爆炸,i 1先爆炸然后i爆炸,i-1和i 1都爆炸然后i再爆炸; 这样需要考虑复杂很多!

总结

题目1 简单贪心,因为两个操作的控制要素不多,比较容易抉择; 题目2 简单模拟,题目的要求和范围都很简单,但是扩展题目的需要一点思路 代码能力; 题目3 暴力计算,枚举区间的做法比较暴力,收获也比较少,优化做法会更佳; 题目4 排序选择,排序简化思考,核心把平均数用和表达出来; 题目5 简化思考,部分决策因素会导致题目很复杂,从结果反推,配合枚举是很好的方式。

0 人点赞