题目一 ddl的恐惧
题目描述
ZJM 有 n 个作业,每个作业都有自己的 DDL,如果 ZJM 没有在 DDL 前做完这个作业,那么老师会扣掉这个作业的全部平时分。
所以 ZJM 想知道如何安排做作业的顺序,才能尽可能少扣一点分。
请你帮帮他吧!
input
输入包含T个测试用例。输入的第一行是单个整数T,为测试用例的数量。
每个测试用例以一个正整数N开头(1<=N<=1000),表示作业的数量。
然后两行。第一行包含N个整数,表示DDL,下一行包含N个整数,表示扣的分。
output
对于每个测试用例,您应该输出最小的总降低分数,每个测试用例一行。
example
input 1
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 1
0
3
5
做法与思路
先确定贪心策略。要保证扣分最少,即得分最多。
只要求我们尽可能的完成分值高的任务,但要意识到分值高的任务并不是完成的越早就越好。考虑如下情景,A任务ddl为20日,分值10分,B任务ddl为30日,分值为100分,现在是第20日,我们还是要选择A任务,因为过了ddl后完成任务没有意义,而留给B的时间还有很多。
所以我们考虑策略的时候不能单纯考虑分值,还要综合考虑ddl,要保证选择了该任务之后对之后的任务产生的影响尽可能的小,即该任务选择后不会使其他本可以完成无法完成。依旧是上一个例子,选择A任务后还剩余10天,不会给B任务带来太大的影响。
而如何保证影响小呢,显然是完成的越靠后对之后的影响越小,如果某个任务放在第一天,那么接下来的所有任务都要延后一天。从这个角度考虑,把一个任务放在它ddl的当天对之后的影响最小。
这样我们的贪心策略就差不多形成了,首先要保证分高者得,然后保证选择该任务后对其他任务的影响尽可能小,即把该任务完成时间尽可能的向它的ddl靠拢。
秉持着这样的策略,我们可以按天数倒序选择任务,这样能保证产生的影响最小;先选择ddl最晚的这一天,将ddl在这一天的所有任务加入一个容器,从中选取分值最高的放在这一天,然后以此类推一直到第一天。
这个过程中我们要不断向容器内塞任务的分数,并且要取出最大值,显然大根堆是最适合的数据结构。可以使时间复杂度为nlogn
代码
#include <iostream>
#include <algorithm>
#include <queue>
using namespace std;
/* run this program using the console pauser or add your own getch, system("pause") or input loop */
struct task{
int p;//扣分
int ddl;
task()
{
p=0;ddl=0;
}
task(int a,int b)
{
p=a;ddl=b;
}
};
bool bijiao(task a,task b)
{
return a.ddl<b.ddl;
}
task z[2000];
int sum;
int main(int argc, char** argv) {
int numble;
cin>>numble;
for(int i=0;i<numble;i++)
{
priority_queue<int> tem;
sum=0;
int shu;
cin>>shu;
for(int j=1;j<=shu;j++)
{
cin>>z[j].ddl;
}
for(int j=1;j<=shu;j++)
{
cin>>z[j].p;
sum+=z[j].p;
}
z[0]=task(-1,-1);
sort(z,z+shu+1,bijiao);
//cout<<z[3].p<<endl;
int s=shu;//记录现在走到哪一个任务
for(int j=z[shu].ddl;j>=1;j--)
{
while(z[shu].ddl==j)
{
tem.push(z[shu].p);
shu--;
}
if(!tem.empty())
{
//cout<<"top为"<<tem.top()<<endl;
sum-=tem.top();
tem.pop();
}
}
cout<<sum<<endl;
}
return 0;
}
题目二 四个数列
题目描述
ZJM 有四个数列 A,B,C,D,每个数列都有 n 个数字。ZJM 从每个数列中各取出一个数,他想知道有多少种方案使得 4 个数的和为 0。
当一个数列中有多个相同的数字的时候,把它们当做不同的数对待。
请你帮帮他吧!
input
第一行:n(代表数列中数字的个数) (1≤n≤4000)
接下来的 n 行中,第 i 行有四个数字,分别表示数列 A,B,C,D 中的第 i 个数字(数字不超过 2 的 28 次方)
output
输出不同组合的个数。
example
input 1
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 1
5
做法与思路
这个题一上来的想法肯定是穷举,复杂度为n的四次方,无法通过,但是可以用来验证自己其他方法的正确性。
考虑其他做法,可以将所选的四个数进行二分,a+b+c+d=0即a+b=-c-d,分为a+b与-c-d,先穷举a+b,把结果存入容器T中。然后穷举-c-d,检查其结果在T中出现过几次,最后累加即可。
值得一提的是检查-c-d这个过程,一开始我想用unordered_map,但是不支持,作罢。然后换成了map,结果超时,这使我认为logn复杂度的查询方式是无法通过该题的,然后想了很久如何解决,还是没想到有效的更低复杂度的办法。最后死马当活马医自己写了二分查找,然后就过了…以后能不借助stl就不借助了。
代码
#include <iostream>
#include <cmath>
#include <algorithm>
using namespace std;
/* run this program using the console pauser or add your own getch, system("pause") or input loop */
int judge[4000*4000];
int main(int argc, char** argv) {
int a[4000],b[4000],c[4000],d[4000];
// cout<<pow(2,28);
int sum=0;
int numble;
cin>>numble;
for(int i=0;i<numble;i++)
{
cin>>a[i];
cin>>b[i];
cin>>c[i];
cin>>d[i];
}
for(int i=0;i<numble;i++)
{
for(int j=0;j<numble;j++)
{
judge[i*numble+j]=a[i]+b[j];
}
}
sort(judge,judge+numble*numble);
for(int i=0;i<numble;i++)
{
for(int j=0;j<numble;j++)
{
int s=-c[i]-d[j];
int l=0,r=numble*numble-1,middle=(l+r)/2;
while(l<=r)
{
middle=(l+r)/2;
if(judge[middle]==s)
break;
else
{
if(judge[middle]>s)
r=middle-1;
else l=middle+1;
}
}
if(judge[middle]==s)
{
int r=middle,l=middle;
while((judge[r]==s)&&(r<numble*numble)) r++;
while((judge[l]==s)&&(l>=0)) l--;
sum+=r-l-1;
}
}
}
cout<<sum;
return 0;
}
题目三 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 的中位数
example
input 1
4
1 3 2 4
3
1 10 2
output 1
1
8
做法与思路
最直接的办法自然是枚举,复杂度为n的平方,但是n高达1e5,所以放弃该做法。我们可以换个角度“验证”某数是不是中位数。
中位数的位置在数组的中间,我们可以进行二分查找,如果它的位置恰好是中位数的位置则满足条件。
这里首先要去掉绝对值号,先对数组进行排序,排序之后的a[n-1]-a[0]即为中位数查找的右侧边界。对于每个中位数,查询满足a[j]-a[i]<=p的个数,即查找在差数组中p的位置,这里可以对式子进行变形,将其转变为a[j]<=p+a[i],寻找有多少组ij。即查询该数字在数组中的位置,如果小于中位数位置,则在左侧查找;否则在右侧查找,注意这里不能等于就直接输出,要保证p必须是能够在数组中取到的数,所以要筛查到左右边界重合位置。
最后还有一点需要注意,代码中的sum+=sum2-i;在穷举i查找j的过程中,要用sum2-i,因为形成的每一次的i和j是生成了a[i+1]-a[i],a[i+2]-a[i]一直到a[sum2]-a[i],一开始我忽略了这一点调错了好久。
代码
#include <iostream>
#include <algorithm>
using namespace std;
/* run this program using the console pauser or add your own getch, system("pause") or input loop */
int a[100001];
int n,ans;
int main(int argc, char** argv) {
while(scanf("%d",&n)!=EOF)
{
for(int i=0;i<n;i++) scanf("%d",&a[i]);
//cout<<1<<endl;
sort(a,a+n);
int mid_index=(n*(n-1)/2+1)/2;
// cout<<"中位数位置"<<mid_index<<endl;
int pr=a[n-1]-a[0],pl=0;
int p=(pr+pl)/2;//记录中位数的搜索边界
//cout<<1;
while(pl<=pr)
{
//cout<<1<<endl;
int sum=0;
p=(pr+pl)/2;
for(int i=0;i<n-1;i++)//统计有几个小于p的
{
//cout<<1<<endl;
int r=n-1,l=0,m=(r+l)/2;//记录j的搜索边界
int sum2=-1;//有几个j满足条件
while(l<=r)
{
m=(r+l)/2;
if(a[m]<=a[i]+p)
{
sum2=m;
l=m+1;
}
else if(a[m]>a[i]+p)
{
r=m-1;
}
}
// if(sum2!=-1)
sum+=sum2-i;
}
// cout<<"现在的sum是"<<sum<<endl;
if(sum<mid_index)
{
pl=p+1;
}
else if(sum>=mid_index)
{
ans=p;
pr=p-1;
}
// cout<<"左"<<pl<<"右"<<pr<<endl;
}
printf("%d\n",ans);
}
return 0;
}