A-DDL的恐惧
问题描述
ZJM 有 n 个作业,每个作业都有自己的 DDL,如果 ZJM 没有在 DDL 前做完这个作业,那么老师会扣掉这个作业的全部平时分。
所以 ZJM 想知道如何安排做作业的顺序,才能尽可能少扣一点分。
请你帮帮他吧!
Input
输入包含T个测试用例。输入的第一行是单个整数T,为测试用例的数量。
每个测试用例以一个正整数N开头(1<=N<=1000),表示作业的数量。
然后两行。第一行包含N个整数,表示DDL,下一行包含N个整数,表示扣的分。
Output
对于每个测试用例,您应该输出最小的总降低分数,每个测试用例一行。
Sample 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
Sample Output
0
3
5
Hint
上方有三组样例。
对于第一组样例,有三个作业它们的DDL均为第三天,ZJM每天做一个正好在DDL前全部做完,所以没有扣分,输出0。
对于第二组样例,有三个作业,它们的DDL分别为第一天,第三天、第一天。ZJM在第一天做了第一个作业,第二天做了第二个作业,共扣了3分,输出3。
解题思路
这道题也是很明显的贪心。要让总扣分最少,就要优先满足分值大的,尽量保证它们能在DDL前完成。
首先创建一个任务安排数组schedule,对每个任务按score从大到小排序,依次对所有任务根据其ddl,将其安排入schedule数组,安排准则是,查询能否将该任务插入ddl那一天,若那一天已经安排过分值更高的task,继续向前搜索直到找到空闲的一天,将此task插入schedule,并更新那一天的标记为1,表示那一天已被安排任务。对于直到找到一天都无法插入的task来说,它就是不得不被zjm忍痛割爱的小可怜(谁叫你sore太小,对一心只想扣更少分的zjm价值不大呢),这个task对应的score就是不得不被扣的分(鱼与熊掌不可兼得啊),加入min_loss中。
代码
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;
struct task {
int ddl;
int score;
bool operator<(const task &t) const {
return score>t.score;
}
};
bool schedule[2000];
task comb[1000];
int descent(int n) {
int min_lose=0;
for(int i=0;i<n;++i) {
scanf("%d",&(comb[i].ddl));
}
for(int i=0;i<n;++i) {
scanf("%d",&(comb[i].score));
}
sort(comb,comb+n);
for(int i=0;i<n;++i) {
int d=comb[i].ddl;
int j=d;
for(j=d;j>0;--j) {
if(schedule[j]==0) {//找到空闲可插入的一天
schedule[j]=1;
break;
}
}
if(j==0) min_lose+=comb[i].score;//未找到不得不被扣分
}
return min_lose;
}
int main() {
int T=0;
scanf("%d",&T);
while(T--) {
int n;
scanf("%d",&n);
printf("%d\n",descent(n));
memset(schedule,0,sizeof(schedule));//执行完一组数据初始化数组
}
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
输出不同组合的个数。
Sample 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
Sample Output
5
Hint
样例解释: (-45, -27, 42, 30), (26, 30, -10, -46), (-32, 22, 56, -46),(-32, 30, -75, 77), (-32, -54, 56, 30).
解题思路
见到这道题毫无疑问首先想到的肯定是暴力枚举,四重循环,时间复杂度是O(n4),n<=4000,会TLE。
因此我们能不能不枚举这么多?既然要四个数的和为零,我们是不是能把四个数二分,使两个数的和等于另外两个数的相反数,求每两个数的和只需要两次循环,O(n²)的复杂度可以接受!
因此我们先枚举A数组和B数组的和,存入数组sum_AB并排序,在枚举C、D的和的时候计算它的相反数在sum_AB数组中出现的次数。
再进一步,计算一个数在有序数组中出现的次数不就是找它在这个数组中第一次和最后一次出现的位置吗!
而找位置又是我们熟悉的有序数组内的二分查找,到这里这个问题就迎刃而解啦!
代码
#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
int A[4000];
int B[4000];
int C[4000];
int D[4000];
int sum_AB[16000000];
int search1(int c,int n) {//查找第一个位置
int l=0,r=n*n-1,ans=-1;
while(l<=r) {
int mid=(l+r)>>1;
if(sum_AB[mid]==c) {
ans=mid;
r=mid-1;
}
else if(sum_AB[mid]<c) l=mid+1;
else r=mid-1;
}
return ans;
}
int search2(int c,int n) {//查找最后出现位置
int l=0,r=n*n-1,ans=-1;
while(l<=r) {
int mid=(l+r)>>1;
if(sum_AB[mid]==c) {
ans=mid;
l=mid+1;
}
else if(sum_AB[mid]<c) l=mid+1;
else r=mid-1;
}
return ans;
}
int find1(int n) {
int count=0;
for(int i=0;i<n;++i) {
for(int j=0;j<n;++j)
sum_AB[n*i+j]=A[i]+B[j];
}
sort(sum_AB,sum_AB+n*n);
int sum_CD=0;
for(int i=0;i<n;++i) {
for(int j=0;j<n;++j) {
sum_CD=C[i]+D[j];
int start=search1(-sum_CD,n);
int end=search2(-sum_CD,n);
if(start!=-1&&end!=-1) //若找到
count=count+end-start+1;
}
}
return count;
}
int main() {
int n;
scanf("%d",&n);
for(int i=0;i<n;++i) {
scanf("%d %d %d %d",A+i,B+i,C+i,D+i);
}
printf("%d\n",find2(n));
return 0;
}
心中的疑问(???)
我不想二分两次分别找起始位置和末尾位置,我想只用search()找到起始位置,再向后搜索末尾位置count++,这种方法为什么每次都wa啊???(小朋友确实有很多???呜呜呜)
int find2(int n) {
int count=0;
for(int i=0;i<n;++i) {
for(int j=0;j<n;++j)
sum_AB[n*i+j]=A[i]+B[j];
}
sort(sum_AB,sum_AB+n*n);
int sum_CD=0;
for(int i=0;i<n;++i) {
for(int j=0;j<n;++j) {
sum_CD=C[i]+D[j];
int start=search1(-sum_CD,n);
if(start!=-1) {//找到
while(sum_AB[start]==-sum_CD) {//开始向后搜索
count++;
start++;
}
}
}
}
return count;
}
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 的中位数
Sample Input
4
1 3 2 4
3
1 10 2
Sample Output
1
8
解题思路
首先想到的肯定是暴力枚举算法,算出ans数组,再取它的中位数,可理想很丰满,枚举的复杂度为O(n²),n<=1e5,超时。
我们现在要换一个思路——给定一个数P,如何判断它是不是一个数组的中位数?答案是计算它在这个数组中从小到大的名次即可!
接下来的问题是如何计算它在ans中的名次,首先明确的是我们不能将ans枚举出来。ans由|cat[i]-cat[j]|产生,首先将cat从小到大排序可以帮助我们去掉绝对值,ans就由cat[i]-catj产生。接下来只需要计算cat[i]-cat[j]<=p的二元组对数即可。移项我们有cat[i]<=cat[j]+p,接下来我们可以通过对下标j的从前往后枚举,计算满足条件的下标i的个数,枚举的过程中仍然可以二分查找!
枚举结束后,得到当前p的名次rank。对于p的名次,会出现这样一个问题,即如果rank大于中间位置,是否就等价于这样的p大于中位数,我们在对p二分枚举的过程应该-1?
回答是否定的。考虑这样一个序列:
2 3 3 3 4 5
当我们按上述方法计算3的名次时,rank=4(即最后一个3对应的位次),而中间位置mid为3,3的确是中位数,但我们枚举出来的位次大了,怎么解决。(每个p计算出的rank始终是所有相等的p在序列中最后的那个p对应的位置,这也意味着rank<mid代表p确实小了),也即rank大于中间位置mid的p,也可能是中位数,这种情况的p在二分枚举中不能舍弃。
解决方法是:对所有rank>=mid的p统一处理。先考虑p确实>中位数的情况,那么我们在此令r更新为p-1没有任何问题;再考虑此时p=中位数的情况,假设我们现在更新r=p-1,那么r始终<中位数,在今后的枚举计算rank中,所有p的rank都会<mid,r永远不会更新,始终=中位数-1。l会一直更新,二分最终的情况是l=r=中位数-1,此时rank仍然<mid,就会再次更新l=p+1,这时l=中位数>r,退出循环。直接输出l即为所求。
代码
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
int cat[100010];
int find(int l,int r,int c) {//二分查找i的最大位置
int ans=-1;
while(l<=r) {
int mid=(l+r)>>1;
if(cat[mid]<=c) {
ans=mid;
l=mid+1;
}
else r=mid-1;
}
return ans;
}
int position(int mid,int n,int p) {
int rank=0;
for(int i=0;i<n-1;++i) {
int c=cat[i]+p;
int num=find(i+1,n-1,c);
if(num>0) {//每次枚举更新名次
rank=rank+num-i;
}
}
if(rank>=mid) return 1;
else return 0;
}
int main() {
int n;
while(~scanf("%d",&n)) {
for(int i=0;i<n;++i) {
scanf("%d",cat+i);
}
sort(cat,cat+n);
int m=n*(n-1)/2+1;
int mid=m>>1;
int l=0,r=cat[n-1]-cat[0],p=0;
while(l<=r) {
p=(l+r)>>1;
int sign=position(mid,n,p);
if(sign==1) r=p-1;//rank>=mid
else l=p+1;//rank<mid
}
printf("%d\n",l);
memset(cat,0,sizeof(cat));//初始化cat数组
}
return 0;
}