习题5-3(打牌问题、动态规划最大方案问题)

习题5-3

纸牌

  • 给你n张牌,对手同样有n张牌
  • 所有的牌都互不相同
  • 每轮你和你的对手都打出一张牌
  • 牌的点数更小的获胜
  • 问你运气最好能获胜几轮

分析

  • n<=10^5
  • 怎么枚举不同的情况

解法1

  • 我出一张牌,对方出一张牌,牌就没了
  • 可以用一张表格来表达流程,表格确定,比赛就比完了
  • 枚举完所有的表格,找到赢得最多的即为答案
  • 枚举我可能出的牌,枚举地方可能出的牌
  • 我的复杂度O(n!) 敌方O(n!)
  • 总共O((n!)^2)
  • 重要的是你的牌的对局关系,你这次牌第几轮打出的是没有影响的
  • 固定一方的出牌顺序,所以只需要枚举一列的就可以找出所有的情况,这也就是本质不同的出牌顺序,复杂度变为O(n!)
  • 对对方的牌进行排序,假设第一张牌是最厉害的,
  • 采用减而治之的思路,考虑我方第一张牌出什么牌?
  • 1.如果我方最厉害的牌不能战胜对方最厉害的牌,则我方所有的牌都不能赢,则拿我方最差的一张牌来对你,这是一种很明显的贪心思想
  • 2.因为不可能出现平局,如果我方最厉害的一张牌能战胜对面最厉害的那张牌,此时这张牌可以看成负INF,换个视角,在对方看来,对方使出最大的那张牌是最不划算的,所以我方就应该拿最厉害的牌来消耗对面最厉害的牌
程序设计
  • 第一步,对对方所有的牌进行排序
  • 同时需要对自己所有的牌进行排序,拿最大值比较,同时需要找到最小值,换言之,对我们的牌堆,需要能够快速查询最大值和最小,以及快速删除最大值和最小值,通过排序,加前后两个指针就可以实现这些操作
代码分析
  • for (int i = 1; i <= 2*n; i++) {
                arr[i] = i;
            }
            for (int i = 1; i <= n; i++) {
                exsit[arr[scanner.nextInt()]] = true;
            }
    
  • 读入我方的牌,同时表示出现的牌,则没出现的牌就是对方的牌

  • int ans = 0;
            int flag = 1;
            for (int i = 1; i <= n && flag<=n; i++) {
                if (a[i]>b[flag]){
                    ans++;
                    flag++;
                }
            }
    
  • 用flag指针来标记我方最厉害的牌

  • 当我方最厉害的牌比对方最厉害的牌厉害,则消耗一张最厉害的牌

  • 当我方最厉害的牌赢不了对方时,就不消耗,并不需要去记录尾指针的值

标程
  • public static void main(String[] args) {
            Scanner scanner = new Scanner(System.in);
    
            int n = scanner.nextInt();
            boolean[] exsit = new boolean[2*n+1];
        	// 对方牌的点数
            int[] a = new int[n+1];
        	// 我方牌的点数
            int[] b = new int[n+1];
            int[] arr = new int[2*n+1];
            for (int i = 1; i <= 2*n; i++) {
                arr[i] = i;
            }
            for (int i = 1; i <= n; i++) {
                exsit[arr[scanner.nextInt()]] = true;
            }
            int p = 1;
            int q = 1;
            for (int i = 1; i <= 2*n; i++) {
                if (exsit[i]){
                    b[p++] = arr[i];
                }else {
                    a[q++] = arr[i];
                }
            }
        	// 排序
            Arrays.sort(a);
            Arrays.sort(b);
            int ans = 0;
            int flag = 1;
            for (int i = 1; i <= n && flag<=n; i++) {
                // a[i]是对方当前最厉害的牌
                // b[flag]是我方当前最厉害的牌
                // 末端位置没有记录,发现不需要查询最小值是多少,不关心点数,所以不记录
                // 害怕越界,但是对方的牌每轮都在进行,对方牌发完也就比完了
                if (a[i]>b[flag]){
                    ans++;
                    flag++;
                }
            }
            System.out.println(ans);
        }
    

题目的复杂情况

  • 如果牌并不是两两相同,这对于题目解决是很有用的信息
  • 即有可能出现平局
  • 如果遇到两两相等的情况了,出现了一个争议
  • 发现一个问题,先不解决它,先去解决别的问题,后面自然解决它,岂不美哉?
  • 先搁置这个情况,反向去比较,从末端去比较最弱的牌,如果我方最弱的牌等战胜对方最弱的牌,就用这张牌去消耗,如果我方最弱的牌不能战胜对方最弱的牌,就不如去送死,保留自己的战斗力,则用这张牌去消耗对方最强的牌,这样就可能化解掉争议,解决之前的问题
  • 更复杂的情况,上面遇到争议,下面也遇到争议,即两边都有争议,此时如何解决?
  • 假设平局可以得0.5分,我方最强的牌和对方最强的牌平局,我方最弱的牌和对方最弱的牌也是平局,此时有一种一般情况,即拿最强对最强,最弱对最弱,此时可以得到1分,最糟糕的情况是我方最强对地方最弱,此时仍然可以得到1分,发现最糟糕的情况仍然可以得1分,所以最后就采用最糟糕的情况的出牌方式。

青蛙

  • 数轴上给定n个点,有n个坐标,同时有n个分数
  • 初始需要选择起点, 同时选择一个方向,需要往这个方向跳跃,而且每次跳跃的距离不能少于上次的跳跃距离,并且跳跃方向必须与上一次保持一致
  • 求尽可能大的坐标累加分数

暴力法

  • 枚举起点
  • 枚举下一步跳到哪个点,同时记录分数总和
  • 需要判断这个点能不能跳
  • 能跳则继续搜索,不能跳就放弃这个点,继续枚举下一个点
记录的信息
  • 当前达到的点[1,n]

  • 上一步跳跃的距离(这个数可能非常大,换一种记录方法,上一次到达的点[1,n])

  • 分数总和(条件相同情况下,越大越好)(小的信息根本不用记,直接丢掉)

局限性
  • n来到1000,搜索算法(暴力法)时间复杂度是行不通的
分析
  • 分析记录的信息,是否发现使用什么算法?

  • 对于一个状态,记录的信息只有两个(前两个)(可以接受的量级),还有一个信息是用来判断状态好坏的

  • 所以是动态规划算法,前两个需要记录的,规模比较小的变量,当做状态的表示,我们把分数当做状态的函数值,转移怎么设计?按照搜索思路去设计

动态规划
  • dp(i)(j)表示当前节点是i,上一个节点为j时最大得分
  • x[i]-x[j]上一次跳跃的距离
  • 枚举下一个节点k
  • if(x[k]-x[i]>=x[i]-x[j])
  • dp(k)(i) = max(dp(k)(i),dp(i)(j)+s(k)),这是一种push的转移
  • 边界条件是dp(i)(j)=s(i)+s(j),因为第一步跳跃不受限制
  • 时间复杂度是O(n^3),空间复杂度和状态树同阶,是n的平方
代码分析
  • for(int i = 1; i <= n; ++i){
            int x, y;
            scanf("%d%d", &x, &y);
            a[i] = pair<int, int> (x, y);
        }
    
  • a[i]中x是坐标,y是分数

  • for(int round = 0; round < 2; ++round){
            sort(a + 1, a + n +1);
            for(int i = 1; i <= n; ++i){
                dp[i][i] = a[i].second;
                for(int j = 1; j < i; ++j){
                    dp[i][j] = 0;
                    for(int k = j; k && 2 * a[j].first <= a[i].first + a[k].first; --k)
                        dp[i][j] = max(dp[i][j], dp[j][k]);
                    ans = max(ans, (dp[i][j] += a[i].second));
                }
            }
            for(int i = 1; i <=n; ++i)
                a[i].first = -a[i].first;
        }
    
  • 进行两轮的算法处理,每一次都是从左往右跳,但是第二轮对坐标轴进行了相对于原点的反转

  • sort(a + 1, a + n +1);
    
  • 每一轮的开始都对坐标进行排序(只对x排序)

  • for(int i = 1; i <=n; ++i)
                a[i].first = -a[i].first;
    
  • 第一轮的最后对所有的坐标都取负,相当于把数轴相对于原点进行了翻转

  • dp[i][i] = a[i].second;
    
  • 假设第一步是从i跳到i(边界条件初始化)

  • 此处的状态转移是采用的pull的转移方式,即当前这种状态可以由哪些状态达到

  • dp[i][j] = 0;
    
  • 初始化前一步跳动的记录,

  • 2 * a[j].first <= a[i].first + a[k].first
    
  • a[j].first-a[i].first<=a[k].first-a[j].first时,前一步的跳跃距离小于等于当前这一步的跳跃距离时,找到可以跳的得到的最大分数

  • dp[i][j] = max(dp[i][j], dp[j][k]);
    
  • 此处没有加上这一步的跳跃分数,是放在了更新答案时统一加上,

标程
  • #include <bits/stdc++.h>
    using namespace std;
    const int N = 1003;
    
    int n;
    
    
    int dp[N][N];
    
    int main()
    {
        pair<int, int> a[N];
        scanf("%d", &n);
        for(int i = 1; i <= n; ++i){
            int x, y;
            scanf("%d%d", &x, &y);
            a[i] = pair<int, int> (x, y);
        }
        int ans = 0;
        // 两轮处理
        // 第二次将坐标按原点翻转,这样就相当于左右跳都考虑了
        for(int round = 0; round < 2; ++round){
            // 对坐标排序
            sort(a + 1, a + n +1);
            for(int i = 1; i <= n; ++i){
                // 设置边界条件
                // 第一步是i跳到i,下一步随便跳,肯定大于0
                dp[i][i] = a[i].second;
                for(int j = 1; j < i; ++j){
                    dp[i][j] = 0;
                    // 更前一个节点k,先跳到j,再跳到i
                    // 2 * a[j].first <= a[i].first + a[k].first   小小的剪枝,当k跳到j必须满足从j跳到i的要求
                    for(int k = j; k && 2 * a[j].first <= a[i].first + a[k].first; --k)
                        // 这是一种pull的转移
                        // 求出一个最大的dp[j][k]
                        dp[i][j] = max(dp[i][j], dp[j][k]);
                    // 对于dp[i][j],加的是s[i],不管前面从哪里转移来,加的都一样
                    ans = max(ans, (dp[i][j] += a[i].second));
                }
            }
            // 坐标翻转
            for(int i = 1; i <=n; ++i)
                a[i].first = -a[i].first;
        }
        printf("%d\n",ans);
        return 0;
    }
    
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值