PTA四题
引言
今天学校上机,我将一些思路和大家分享一下
题目五要求:
本题和洛谷P1006一致,感兴趣的同学也可以康康.
整体思路一:(暴力)DP
A把纸条传递给B,B在传回来的过程,如果我们反着看不就是相当于A同时传递了两张纸条吗,因为我们如果考虑两个方向同时DP,这个复杂度绝必是非常大的,就单单判断二者是不是访问了一个节点就很困难。所以我们不妨反着看,从B到A正是A到B的逆过程,这样,两条路同时进行,一点但同时走到了一个点就只保留一个点的好感度就行,因为只有一条路能访问到这里。
1、定状态
我们同时从(1,1)出发两条路,他们分别走到(i,j)(k,l)所以我们必须表示他们走到哪里了,所以暴力四个状态,前两个表示第一条路走到了哪里,后两个表示另一条路走到哪里。
2、初始化
因为要算好感度值,最开始都没有,所以为0.
3、找转移
记住:找转移的时候抓住“大的区间都是通过小的区间转移得来的再加上他本身的消耗”,本题当走到了(i,j)(k,l)自然的消耗就是a[i][j]+a[k][l],是固定的,所以寻找最优就要看子区间,子区间最优,该区间最优,这就是典型的区间DP,状态转移模板如下:
再回到这个题,因为dp[i][j][k][h]他的子状态就四个,每个点只能两个方向,向右向下,所以2*2=4个子状态,这里就直接写,不用枚举了。
1、当1节点向右到了本节点时,2节点可能是向下向右来的dp[i - 1][j][k - 1][h] dp[i - 1][j][k][h - 1] 。
2、当1节点向下到了本节点时,2节点可能是向下向右来的dp[i][j - 1][k - 1][h] dp[i][j - 1][k][h - 1] 。
所以这四个子状态中找到最优的解找到就行,再加上本次消耗,所以取最大值。
4、觅答案
当两个点都到达终点即可,即 dp[m][n][m][n]
具体代码
注意,题中说每个节点只能访问一次,所以一旦两个路径当前点冲突了,那么只留下一次的好感度
#include <iostream>
#include <algorithm>
using namespace std;
//暴力DP
int dp[51][51][51][51] = { };
int a[51][51];//矩阵
int m, n;//行,列
int main()
{
cin >> m >> n;
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
scanf("%d", &a[i][j]);
}
}
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
for (int k = 1; k <= m; k++) {
for (int h = 1; h <= n; h++) {
dp[i][j][k][h] = max(max(dp[i - 1][j][k - 1][h], dp[i - 1][j][k][h - 1]), max(dp[i][j - 1][k - 1][h], dp[i][j - 1][k][h - 1])) + a[i][j] + a[k][h];
//四个子状态选最优(即最大)+本次消耗
//如果本区间消耗和子区间选取有关系,那么就把本次消耗
//放进子区间选取中,保证综合下来的结果最优
if (i == k && j == h) {//同一点只能走一次
dp[i][j][k][h] -= a[k][h];//如果访问冲突了,那么好感度只加一次
}
}
}
}
}
cout << dp[m][n][m][n];//两个点都到终点就是结果
}
(所有代码均已运行无误)
经测试,该代码运行情况是(经过多次测试所得最短时间):
这个想法很直接很简单,但是时间复杂度至少是O(n^4),也十分消耗空间,做了大量冗余计算,一旦卡数据就完蛋了。
整体思路二:三维DP
我们的上一个方法,第一:是做了很多冗余计算的,当第一条路选择了(i,j),那么第二条路就根本不可能再次去选择(i,j),那么,我们只需要选择剩余可选择的的点就行,来降低复杂度;第二:维度太大了,循环层数太多了,时空复杂度高的一批,所以要降维来降低时空复杂度。
1、定状态
首先,我们的目标就是减少维度,维度越大,那么时空复杂度就越大,通过上图可知,从源点开始,每次可推进的点必在一个对角线上,和k=i+j是固定的,所以我只需要知道两条路径端点所在的行,就可以通过k推知列号,所以 k,i,j共三个状态。
2、初始化
因为要算好感度值,最开始都没有,所以为0.
3、找转移
记住:找转移的时候抓住“大的区间都是通过小的区间转移得来的再加上他本身的消耗”,本题当走到了(i,k-i)(j,k-j)自然的消耗就是a[i][k - i] + a[j][k - j],是固定的,所以寻找最优就要看子区间,子区间最优,该区间最优,这就是典型的区间DP,状态转移模板如下:
当走到dp[k][i][j]时,他的子区间是啥,就是上一次推进即k-1的诸多状态,通过上次的诸多状态中找到最优再加之我们本次消耗,就是本次的最优解,注意,i,j是行,在看上一次推进子状态时就是看这两次之间行的关系(行只能向下)
子状态1:
dp[k - 1][i][j]:两次行关系不变
子状态2:
dp[k - 1][i - 1][j]:第一个路径的行下降一个到了i,j不变
子状态3:
dp[k - 1][i][j - 1]:第二个路径的行下降一个到了j,i不变
子状态4:
dp[k - 1][i - 1][j - 1]:第一个路径的行下降一个到了i,第二个路径的行下降一个到了j
综上:在四个子状态中找到最优+自身消耗即可
4、觅答案
在k=m+n-1时,就差一次推进就到了汇点,汇点没有好感度,所以到了倒数第二步就是答案,即dp[m + n - 1][m - 1][m];
具体代码
注:
1、为了减少冗余计算,所以选过了的节点不要重复,所以在实现时候,i行占有了,那么相应的这次推进的点(i,k-i),另一条路径就不要选这个点了,因为他已经帮过一次忙了,另一条路径就要向后枚举还可用的点,所以j=i+1作为初始
2、看上方找状态画的图,k=3的时候,能推进到的列最多k-i=1,所以要越界判定。
#include <iostream>
#include <algorithm>
using namespace std;
//三维DP
int dp[200][200][200] = {};
int a[51][51];
int m, n;
int main()
{
cin >> m >> n;
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
scanf("%d", &a[i][j]);
}
}
for (int k = 3; k < m + n; k++) {//k从3开始到m+n-1,不懂看我上方的图
for (int i = 1; i <= m; i++) {//第一路径的行从头到尾都有可能
for (int j = i + 1; j <= m; j++) {//为了减少冗余计算,i行走过了,那么我就从j=i+1走
//这样读者就会有疑问为,i=1时,j不能在1行出现,但是当i=2,j就可以出现在1行了,
//但是我们换过来想,这不和i=1时重复了吗,因为i,j只是任意的两行,没有
//先后之分,但为了代码直观,我们默认i在j上面,其实二者没有严格的位置之分
//所以不能同时让一个人帮两次,所以j在i后面取
if (k - i < 1 || k - j < 1) {//利用列来进行越界判定
continue;
}
dp[k][i][j] = max(max(dp[k - 1][i][j], dp[k - 1][i - 1][j]), max(dp[k - 1][i][j - 1], dp[k - 1][i - 1][j - 1])) + a[i][k - i] + a[j][k - j];
//四个子状态选最优(即最大)+本次消耗
//如果本区间消耗和子区间选取有关系,那么就把本次消耗
//放进子区间选取中,保证综合下来的结果最优
}
}
}
cout << dp[m + n - 1][m - 1][m];
}
(所有代码均已运行无误)
经测试,该代码运行情况是(经过多次测试所得最短时间):
(通过这个时间,和上面一比即可看出降了一维,可以将时间复杂度下降非常大的幅度)
降到三维,时间复杂度是O(n^3),就可以大幅减少复杂度,那么我们如果再通过滚动数组就可以再次降维到二维,那么就会更快,但是博主暂时不太会滚动数组,所以这个等以后俺 b 学会了再更新。
题目四要求
整体思路
这个题就相当于问,给你规定个时间,让你尽可能多的安排人干活,我们按照常识肯定选谁干的快,先选谁,干得快的先干完,然后我才能安排更多的人,这道题就是这种想法。
代码
注:因为要为结构体进行sort,结构体为自定义数据结构,并非内置,所以要告诉sort函数我到底要咋排,所以重载<运算符,告诉我到底咋排序,内部按照什么逻辑,此时<只是个符号,sort会自动调用重载运算符函数。
#include <iostream>
#include <algorithm>
using namespace std;
struct mission {
int stime;
int etime;
int idx;
bool operator < (const mission & node) const {//重载运算符<
return etime < node.etime;//保证从截止期从小到大
}
};
mission a[1000];
int answer = 1;
int n;
int main() {
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> a[i].stime >> a[i].etime;
}
sort(a + 1, a + 1 + n);//将截止期从小到大排序,具体比较方法看重载运算符函数
int pre = a[1].etime;//最快的一定选,作为前驱,动态寻找个数
for (int i = 2; i <= n; i++) {
int temp = a[i].stime;
if (temp > pre) {//如果开始时间大于前驱截止期,就选
answer++;//更新
pre = a[i].etime;
}
else {
continue;
}
}
cout << answer;
}
(所有代码均已运行无误)
经测试,该代码运行情况是(经过多次测试所得最短时间):
题目三要求
整体思路
我们就动态的扫描一遍这个数组就行了,边扫描边判断边更新即可。
代码
注:是"前-后"!!!
#include <iostream>
#include <algorithm>
using namespace std;
int n;
int a[10000];//存放目标数据
int ans = 0;
int main() {
cin >> n;
if (n <= 2) {
cout << n;
return 0;
}
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
int pre = a[1] - a[2];//前驱
int temp_ans = 1;//临时个数,用于记录某一段个数,最后要和答案取最大值
//因为要的是最大长度
for (int i = 2; i <= n; i++) {
int temp = a[i] - a[i + 1];
if (i == n) {//最后一次循环用于更新ans记录用,要不然少更新一次
//倒数第二次更新完就退出了,没来得及更新ans
ans = max(ans, temp_ans);
}
if (temp*pre < 0) {//判定是摇摆队列
pre = temp;//更新
temp_ans++;
}
else {
ans = max(ans, temp_ans);//不是的话更新ans
temp_ans = 1;//这些因为要继续找,所以也要进行更新
pre = temp;
}
}
cout << ans+1;//最后答案老是少一,所以+1输出
}
(所有代码均已运行无误)
经测试,该代码运行情况是(经过多次测试所得最短时间):
题目二要求
整体思路
思路很简单,我贪心最大效益,所以我就让它每次都出现在他的截止期的位置处,如果截止期过大,超过了n,则默认截止期在n即可,如果截止期那个位置处被占用,那么我们就要通过并查集向前寻找他的前驱能插入位置。
代码
注:在实现的时候为了快速向前寻找,所以引入集合树(并查集)思想。
#include <iostream>
#include <algorithm>
using namespace std;
struct mission {
int time;//截止期
int value;//价值
int idx;//序号
bool operator < (const mission & node) const {
return value > node.value;//这是结构体排序,保证从大到小
}
};
bool isVisit[1000] = { false };//判断是否走过这里
mission a[1000];
int answer[1000];//答案
int ano[1000] = { 0 };//并查集
int n;
int val = 0;//总价值
int num = 0;//记录能放进去的总数
int main() {
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> a[i].time;
if (a[i].time > n) {
a[i].time = n;
}
cin >> a[i].value;//初始化
a[i].idx = i;
ano[i] = i;
}
sort(a + 1, a + 1 + n);
for (int i = 1; i <= n; i++) {
bool flag = false;
int item = a[i].time;//查截止期1
int size = ano[item];//在并查集里面找截止期可能的在答案序列的放入位置
if (size == 0) {//是零说明无法放入
continue;
}
if (!isVisit[size]) {//相应位置可以放进去
answer[size] = a[i].idx;
val += a[i].value;
num = size;
ano[size] = ano[size - 1];//并查集更新操作
isVisit[size] = true;
}
else {
while (1) {//我找到的位置放不进去,那么
size = ano[size];//循环在并查集中寻找可以放入的点
if (size == 0) {//0说明放不进去了
flag = true;
break;
}
if (!isVisit[size]) {
break;
}
}
if (flag) {//flag是true说明放不进去
continue;
}
else {
answer[size] = a[i].idx;//放进去了和上面操作一样
val += a[i].value;
num = size;
ano[size] = ano[size - 1];
isVisit[size] = true;
}
}
}
printf("%d\n", val);
int i = 1;
for (; i < num; i++) {//输出(这里可能有点小错误,大家可以忽略这里,主题还是并查集那块)
if (answer[i] != 0) {
printf("%d ", answer[i]);
}
}
printf("%d", answer[i]);
system("pause");
}
(所有代码均已运行无误)
经测试,该代码运行情况是(经过多次测试所得最短时间):
题目一要求
整体思路
完全背包问题,我为了让效益最大化,肯定优先放入单位体积下价值最大的,所以求一下单位体积价值,排序即可,从大到小放入。
代码
#include <iostream>
#include <algorithm>
using namespace std;
struct mission {
double w;//质量
double v;//价值
int idx;//序号
double val;//单位质量价值
bool operator < (const mission & node) const {
return val > node.val;//保证从大到小
}
};
mission item[1000];
int m, n;
double ans = 0;
double answer[1000] = { 0 };
int main() {
cin >> m >> n;
for (int i = 1; i <= n; i++) {
cin >> item[i].w;
item[i].idx = i;
}
for (int i = 1; i <= n; i++) {
cin >> item[i].v;
item[i].val = (item[i].v) / item[i].w;//求一下单位质量价值
}
sort(item + 1, item + 1 + n);//将单位质量价值从大到小排序
for (int i = 1; i <= n; i++) {
if (m == 0) {//背包满了就不装了
continue;
}
double temp_w = item[i].w;//去取出重量
if (m - temp_w >= 0) {//可完全放入
double temp1 = temp_w / item[i].w;//求比值
answer[item[i].idx] = temp1;
ans += item[i].v*temp1;//价值*比值(公式见题目)
m -= temp_w;//更新m
}
else {
double temp1 = m / item[i].w;//求出我当前能拿走的重量占总体比值
answer[item[i].idx] = temp1;
ans += item[i].v*temp1;//用它计算占有总价值的多少
m = 0;
}
}
printf("%g\n", ans);//输出,可忽略
int i = 1;
for (; i < n; i++)
{
printf("%.2f ", answer[i]);
}
printf("%.2f", answer[i]);
system("pause");
return 0;
}
(所有代码均已运行无误)
经测试,该代码运行情况是(经过多次测试所得最短时间):