Week 4 作业 A-ddl的恐惧 B-四个数列 C-TT的神秘礼物
A-ddl的恐惧
题目描述
ZJM 有 n 个作业,每个作业都有自己的 DDL,如果 ZJM 没有在 DDL 前做完这个作业,那么老师会扣掉这个作业的全部平时分。所以 ZJM 想知道如何安排做作业的顺序,才能尽可能少扣一点分。请你帮帮他吧!
输入输出格式及样例
Input
输入包含T个测试用例。输入的第一行是单个整数T,为测试用例的数量。
每个测试用例以一个正整数N开头(1<=N<=1000),表示作业的数量。
然后两行。第一行包含N个整数,表示DDL,下一行包含N个整数,表示扣的分。
Output
对于每个测试用例,您应该输出最小的总降低分数,每个测试用例一行。
Input
3
3
3 3 3
10 5 1
3
1 3 1
6 2 3
7
1 4 6 4 2 4 3
3 2 1 7 6 5 4
Output
0
3
5
Hint
上方有三组样例。
对于第一组样例,有三个作业它们的DDL均为第三天,ZJM每天做一个正好在DDL前全部做完,所以没有扣分,输出0。
对于第二组样例,有三个作业,它们的DDL分别为第一天,第三天、第一天。ZJM在第一天做了第一个作业,第二天做了第二个作业,共扣了3分,输出3。
思路
因为要求最少的扣分,我们采用贪心算法,贪心准则肯定是先处理分扣分最高的,但是仅仅如此因为该任务扣分最多而先处理,它可能影响到更多的任务的安排,所以为了降低每个任务对其他任务的影响,我们让该任务尽可能的踩点做完(就跟踩点上课一样,为了做其他事情而拖延去上课的时间 -_-)。
所以我们先安排扣分最多的任务,每个任务从他的ddl往前寻找空余时间,如果往前没有空闲时间则放弃该任务,
实验代码
#include <iostream>
#include <string.h>
#include <algorithm>
using namespace std;
struct c {
int score;//每个任务的扣分
int ddl;//每个任务的ddl
bool operator<(const c b) {
return score > b.score;//优先处理扣分最多的任务
}
};
bool time[20000];
int main() {
int T; cin >> T;
while (T > 0) {
int n; cin >> n;
int sums = 0;
c* test = new c[2000];
for (int i = 0; i < n; i++)cin >> test[i].ddl;
for (int i = 0; i < n; i++) {
cin >> test[i].score;
sums += test[i].score;//记录总分
}
sort(test, test + n);//按扣分降序排序
int score = 0;//记录得分
for (int i = 0; i < n+1; i++)time[i] = 0;//初始化每一天的空闲情况
for (int i = 0; i < n; i++) {
int j = test[i].ddl;
for (; j > 0; j--) {
if (!time[j]) {
time[j] = 1;//从每个任务当天开始向前安排该任务的处理时间
score += test[i].score;
break;
}
}
}
cout << sums-score << endl;//输出扣分
T--;
}
return 0;
}
B-四个数列
题目描述
ZJM 有四个数列 A,B,C,D,每个数列都有 n 个数字。ZJM 从每个数列中各取出一个数,他想知道有多少种方案使得 4 个数的和为 0。
当一个数列中有多个相同的数字的时候,把它们当做不同的数对待。请你帮帮他吧!
输入输出格式及样例
Input
第一行:n(代表数列中数字的个数) (1≤n≤4000)。
接下来的 n 行中,第 i 行有四个数字,分别表示数列 A,B,C,D 中的第 i 个数字(数字不超过 2 的 28 次方)。
Output
输出不同组合的个数。
Input
6
-45 22 42 -16
-41 -27 56 30
-36 53 -37 77
-36 30 -75 -46
26 -38 -10 62
-32 -54 -6 45
Output
5
Hint
样例解释: (-45, -27, 42, 30), (26, 30, -10, -46), (-32, 22, 56, -46),(-32, 30, -75, 77), (-32, -54, 56, 30).
思路
如果暴力将四个数组的和求出并判断是否等于零,复杂度为n^4,显然超时,所以为了优化,想到二分查找,将四个数组分成两组,将A B两个数组的两两和求出保存在AB数组中,然后找出C D两个数组的两两和的相反数在AB数组中出现的次数,将所有的次数相加就是四个数组和为0的组合数
关于求一个数在数组中出现的次数,用二分法求出该数第一次出现的位置和最后出现的位置,相减极为数量,大幅优化算法。
实验代码
#include <iostream>
#include <algorithm>
using namespace std;
int findx(int x,int n,int *a) {
int l1 = 0, r1 = n - 1,ans1 = -1;
int l2 = 0, r2 = n - 1,ans2 = -1;
while (l1 <= r1 ) {
int mid1 = (l1 + r1) >> 1;
if (a[mid1] <= x) {
ans1 = mid1 + 1;
l1 = mid1 + 1;
}
else r1 = mid1 - 1;
}//找出大于x的第一个元素的索引或等于x的最后一个元素的索引
while (l2 <= r2) {
int mid2 = (l2 + r2) >> 1;
if (a[mid2] >= x) {
ans2 = mid2;
r2 = mid2 - 1;
}
else l2 = mid2 + 1;
}//找出大于等于x的第一个元素的索引
if (ans1 == - 1||ans2 == -1)return 0;//不存在该元素 返回0
else return ans1 - ans2;//返回x的数量
}
int main() {
int n; cin >> n;
int* a = new int[n];
int* b = new int[n];
int* c = new int[n];
int* d = new int[n];
int* ab = new int[n * n];
for (int i = 0; i < n; i++)
cin >> a[i] >> b[i] >> c[i] >> d[i];
for(int i=0;i<n;i++)
for (int j = 0; j < n; j++)
ab[i * n + j] = a[i] + b[j];
sort(ab, ab + n * n);//升序排序
int count = 0;
for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++) {
count += findx(-(c[i] + d[j]), n * n, ab);//找出C D两个数组的每个和的相反数在A B两个数组的和数组中出现的次数,极为能使四个数组元素之和等于0的组合次数
}
cout << count << endl;
return 0;
}
C-TT的神秘礼物
题目描述
TT 是一位重度爱猫人士,每日沉溺于 B 站上的猫咪频道。有一天,TT 的好友 ZJM 决定交给 TT 一个难题,如果 TT 能够解决这个难题,ZJM 就会买一只可爱猫咪送给 TT。任务内容是:给定一个 N 个数的数组 cat[i],并用这个数组生成一个新数组 ans[i]。新数组定义为对于任意的 i, j 且 i != j,均有 ans[] = abs(cat[i] - cat[j]),1 <= i < j <= N。试求出这个新数组的中位数,中位数即为排序之后 (len+1)/2 位置对应的数字,’/’ 为下取整。TT 非常想得到那只可爱的猫咪,你能帮帮他吗?
输入输出格式及样例
Input
多组输入,每次输入一个 N,表示有 N 个数,之后输入一个长度为 N 的序列 cat, cat[i] <= 1e9 , 3 <= n <= 1e5。
Output
输出新数组 ans 的中位数。
Input
4
1 3 2 4
3
1 10 2
Output
1
8
思路
首先,我们很容易可以想到一种暴力做法,就是建一个数组保存所有的 abs(cat[ i ] - cat[ j ]) ,然后根据中位数的索引得到中位数。
这种做法很显然算法复杂度太高,数据太大一定会超时,所以为了优化,我们想到了快速查找里面的二分法,通过二分查找可以极大的降低算法复杂度。
首先,因为我们要求的是新数组里面的中位数,而新数组里面的值是旧数组中任意两个值的差值 的绝对值,所以新数组里的值是介于0 和旧数组最大值最小值的差这两个值之间,而且有序,所以可以采用二分法查找,对于这之间的任意一个值,求出他在新数组里面的名次,如果大于等于中位数的名次,说明它的值大于等于中位数。
其次,为了方便求新数组最大值,我们把原数组升序排序,新数组最大值就是cat[ n ]-cat[ 0 ] ,其他则为cat[ j ] - cat[ i ]( 0 <= i < j < n ),为了方便求p(新数组中的元素)的名次,我们只需求出( cat[ j ] - cat[ i ] ) < p 的所有(i , j) 组合即可简化即为 cat[ j ] - < p + cat[ i ] ,由于 j 也是连续的,所以我们可以迭代 i ,二分 j 求出所有的 j 即可。
总结,本次采用的方法是两次二分法的迭代使用,内部迭代 i 二分 j 求出 p 的名次,外侧二分 p 求出中位数。
值得注意的是:
二分 p 的时候,不要当mid的名次等于中位数的名次就停止查找,因为新数组里面的是不一定是连续整数,二分 p 得到的mid在新数组里面不一定存在,所以求出所有名次等于中位数名次的mid里面的最小值即可(即上课是讲的求第一个名次大于等于中位数名次的值的方法)。
使用scanf替换cin,防止超时
实验代码
#include <iostream>
#include <stdio.h>
#include <algorithm>
using namespace std;
int findx(int x,int n,int* a) {//二分查找找到非递减数组中大于x的第一个元素或等于x的最后一个元素的索引
int l = 0, r = n - 1, ans = -1;
while (l <= r) {
int mid = (l + r) >> 1;
if (a[mid] <= x) {
ans = mid;
l = mid + 1;//去左区间查找
}
else r = mid - 1;//去右区间查找
}
return ans ;
}
int main() {
int n;
while (cin >> n) {
int* cat = new int[n];
for (int i = 0; i < n; i++)
scanf_s("%d", &cat[i]);
sort(cat, cat + n);//对数组进行升序排序
int ans_index = (n * (n - 1) / 2 + 1) / 2;//求出新数组中位数的名次
int l = 0; int r = cat[n - 1] - cat[0];//中位数肯定是位于最大值和最小值之间
int ans = -1;
while (l <= r) {//二分求中位数,只需求新数组中名次>=中位数名次的最小值,即为中位数
//求出名次等于中位数的名次也不一定是中位数,因为通过二分法产生的mid都大于真实的中位数但在新数组中不一定存在,但最小的肯定就是中位数
int mid = (l + r) >> 1;
int sum = 0;
for (int i = 0; i < n; i++) {//对每个mid,遍历i,二分j得到每个i符合条件的j的个数,汇总j的个数就是mid的名次
int count = findx(cat[i] + mid, n, cat);
if (count != -1)sum += count - i;
}
if (sum >= ans_index) {//左区间
r = mid - 1;
ans = mid;
cout << ans << endl;
}
else l = mid + 1;//右区间
}
cout << ans << endl;
}
return 0;
}