习题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; }