A - DDL的恐惧
题目
有 n 个作业,每个作业都有自己的 DDL,如果 没有在 DDL 前做完这个作业,那么老师会扣掉这个作业的全部平时分。求出扣分最少的安排。
(输入包含T个测试用例。输入的第一行是单个整数T,为测试用例的数量。
每个测试用例以一个正整数N开头(1<=N<=1000),表示作业的数量。
然后两行。第一行包含N个整数,表示DDL,下一行包含N个整数,表示扣的分。
要求输出对于每个测试用例,您应该输出最小的总降低分数,每个测试用例一行。)
样例
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
解析
这是一道很典型的贪心问题。如果直接安排的话,可能会比较麻烦,我们不如换一种思想。
每个ddl完成的时间固定,我们为了拿到更多的分数,我们肯定会选择完成分数更高的任务 。这样,我们以分数(在本题为扣分数)作为我们的贪婪指标,来设计我们的贪婪策略。
我们把所有的ddl按照分数降序排列。我们首先拿到分数最高的ddl,对于这个ddl,我们无论把它安排在哪个时间,因为它是分数最高的,我们都找不出另一个ddl替换它可以得到更优的解。之后我们每次取剩下的分数最高的ddl,把它安排在它的截止时间,如果它的截止时间有别的任务,我们就向前找空闲的时间安排它。这样得到的就是最优解。
代码
#include<iostream>
#include<queue>
#include<algorithm>
#include<cstring>
using namespace std;
struct node{
int ddl;
int s;
bool operator < (const node &t) const { return s > t.s;}
} a[10000];
int n;
queue<int> q;
int aa[10000];
int main(){
int T;
scanf("%d", &T);
for(int j = 0 ; j < T ; j++)
{
scanf("%d", &n);
memset(aa, 0, sizeof(aa));
int ans = 0;
int mxd = 0;
for(int i = 1 ; i <= n ; ++i)
{
scanf("%d", &a[i].ddl);
}
for(int i = 1 ; i <= n ; ++i)
{
scanf("%d", &a[i].s);
ans += a[i].s;
}
// 因为本题为扣分,所以这里是升序排序
sort(a + 1 , a + 1 + n);
for(int i = 1 ; i <= n ; ++i)
{
// 找到合适的位置安排
for(int k = a[i].ddl ; k >= 1 ; k--)
{
if(!aa[k])
{
aa[k] = 1;
ans -= a[i].s;
break;
}
}
}
printf("%d\n", ans);
}
}
回顾
这题是贪心问题的一个很好的例子。学长在课上也给出了代码模板(虽然有些写法我没太看懂…),相对来说还是比较容易解决的一道问题。
B - 四个数列
题目
有四个数列 A,B,C,D,每个数列都有 n 个数字。从每个数列中各取出一个数,请问有多少种方案使得 4 个数的和为 0。
样例输入输出
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
解析
这道题在思路上很容易实现。难点在于优化,降低时间复杂度。
最简单的方法,四重循环遍历相加,肯定超时,不提了。
按照分治的思想,我们可以把A + B + C + D = 0 转化成 (A + B)+(C + D)= 0,这样就变成了二重循环,复杂度大大降低。令ab = A + B,cd = C + D ,问题变成了ab + cd = 0 。
之后我们处理ab + cd,可以采用二重循环,不过依然超时。这时我们就需要采用二分的方法来降低时间复杂度。
代码
#include<iostream>
#include<queue>
#include<algorithm>
#include<cstring>
using namespace std;
int a[5000], b[5000], c[5000], d[5000];
int ab[17000000], cd[17000000];
int main() {
int n;
cin >> n;
for (int i = 0; i < n; i++)
cin >> a[i] >> b[i] >> c[i] >> d[i];
int x = n * n;
int tag = 0;
// 分治
for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++)
{
ab[tag] = a[i] + b[j];
cd[tag] = c[i] + d[j];
tag++;
}
int ans = 0;
sort(cd, cd + x);
int left, mid, right;
// 因为超时,这部分重写了很多次...
for (int i = 0; i < x; i++)
{
left = 0;
right = x - 1;
// 二分实现
while (left <= right)
{
mid = (left + right) >> 1;
if (ab[i] + cd[mid] > 0)
{
right = mid - 1;
}
else if (ab[i] + cd[mid] == 0)
{
right = mid - 1;
}
else
left = mid + 1;
}
// 可能往后会出现相同数字的情况
while (left<x && ab[i] + cd[left] == 0)
{
ans++;
left++;
}
}
cout << ans;
}
回顾
总结一下做这道题的历程(天知道我怎么做到-19 的):
正常二重循环找结果,TLE -> 优化采用数组迭代器,TLE -> 换成二分,数组爆栈,RE -> 数组开在外面,AC
简单的一道问题,暴露了很多问题。这个问题的思路很简单,而且代码实现也很简单,可是在我通过了样例和自己准备的数据以后却一直报错了好多次。这让我发现了很多自己的问题。比如我一直习惯
cin >> n 后直接 int a[n],我知道这种写法不太规范,可是直到这次才踩雷。数组爆栈的问题我也听学长讲过,可是一直没有在意。
二分对于升序和降序序列是一种效率很高的查找方式。查找了一些资料,阅读了一下博客,知道STL中是有专门的函数low_bound()和upper_bound()来实现二分查找的(下面C题就用了low_bound)。
C - TT 的神秘礼物
题目
给定一个 N 个数的数组 cat[i],并用这个数组生成一个新数组 ans[i]。新数组定义为对于任意的 i, j 且 i != j,均有 ans[ ] = abs(cat[i] - cat[j]),1 <= i < j <= N。试求出这个新数组的中位数,中位数即为排序之后 (len+1) / 2 位置对应的数字,’/’ 为下取整。
样例输入输出
Sample Input
4
1 3 2 4
3
1 10 2
Sample Output
1
8
解析
同为爱猫人士,ZJM的忙一定是要帮的。考虑到复杂度,这题真的把ans数组求出来一定会超时。那么,我们要想一个不用构建ans数组的方法。
考虑到绝对值,我们把数组cat[i]按升序排序,这样我们取i,j得到的差即为恒正的绝对值。类似上一题,采用分治的思想,令p为中位数,对于ans[ ] = cat[j] - cat[i] = p,我们变形得到cat[j] = cat[i] + p 。因为cat[ ]数组已有序,所以等式两边可以进行二分。
如何判断一个数是不是数组的中位数?只要比它小的数字等于数字总数的一半即可。即,有一半的i,j满足cat[j] - cat[i] < p 即 cat[j] < cat[i] + p 。这样我们就实现了不构建ans数组求出它的中位数。
代码
#include<iostream>
#include<queue>
#include<algorithm>
#include<cstring>
using namespace std;
int a[100000005];
int main(){
int n;
while(cin >> n)
{
for(int i = 0 ; i < n ; i++)
scanf("%d", &a[i]);
sort(a, a + n);
// C2n,x为ans数组的数据总量
int x = n * (n - 1) >> 1;
int left, mid, right;
left = 0;
right = a[n - 1] - a[0];
while(right - left > 1)
{
int tag = 0;
mid = (left + right) >> 1;
// xi = yj + mid,变形
for(int i = 0 ; i < n ; i++)
tag += n - (lower_bound(a, a + n, a[i] + mid) - a); // 下面回顾中提到的一句
if(tag > x / 2)
left = mid;
else
right = mid;
}
cout << left << endl;
}
}
回顾
有了上一题,这题相对来说是比较简单的。这题在完成时遇到的一些小问题主要集中在边界没有判断好导致结果错误上,经过分析很快就解决了。另外刚开始对low_bound()和upper_bound()两个函数产生了误解,以为区别在于升序和降序其实区别在于返回值的位置。
关于位置标记那一句,刚开始写的是tag += lower_bound(a, a + n, a[i] + mid) - a导致错误,是因为没有完全想清楚的自己的代码是想做什么,结果求反了数据。
另外,下次写博客要先写个word,否则这次就不会因为没保存重写了…
下面是CSP模拟部分
A - 咕咕东的奇遇
题目
大致意思是有一个26个字母依次序首尾相连的圆盘,a - z 然后z和a连着。初始时指针指在a上,输入一个字符串,它会依次转向每一个字母。比如你输入zeus,它先会从a转1次到z,然后从z转5次到e,然后从e转11次到u,然后从u转2次到s,共19次。转的时候取次数最少的转法。
输入一个字符串,输出转动的总次数。
解析
字符的加减,模26,大于13和小于等于13两种情况,很简单不再赘述。
代码
#include<stdio.h>
#include<iostream>
#include<string>
#include<cstring>
using namespace std;
int main(){
char s[10000];
cin >> s;
int n = strlen(s);
char tag = 'a';
int total = 0;
for(int i = 0 ; i < n ; i++)
{
int num = s[i] - tag;
if(num < 0)
num += 26;
tag = s[i];
if(num <= 13)
total += num;
if(num > 13)
total += (26 - num);
}
cout << total;
}
B - 咕咕东想吃饭
题目
大致题意如下:
咕咕东买生煎,生煎店有两种购买方式(次数不限),一种是一次买两个,一种是买一个然后送一张券,这张券能且只能在第二天兑换一个生煎。咕咕东会告诉你接下来的n天里他每天要吃的生煎的数量,请问他能不能找到一种合理的购买方案,使得每天都能吃饱且不会浪费。
比如咕咕东4天要吃的生煎数量分别为1,2,3,2,第一天用第二种购买方式,第二天兑换券加第二种购买方式,第三天兑换券加第一种购买方式,第四天第一种购买方式,可以找到购买方案。
输入数据两行,第一行输入的为天数n,第二行n个数字为每天要吃的生煎数量。可以输出YES ,找不到这种购买方式输出NO。
解析
这道题其实很简单,不管咕咕东一天想买多少个生煎,我们先按第一种方案买,直到足够或者剩一个,这是再考虑第二种方案。所以这个问题在处理时对输入的n天的数据进行mod 2 运算,根据结果0和1讨论。用一个flag表示手里是否有券,在mod 2 之前先让数据减去flag。到最后一天,mod 2 为0则有解,为1则无解。
代码
#include<stdio.h>
#include<iostream>
#include<string>
#include<cstring>
using namespace std;
int main(){
int n;
cin >> n;
int a[n];
for(int i = 0 ; i < n ; i++)
cin >> a[i];
int quan = 0;
for(int i = 0 ; i < n ; i++)
{
if((a[i] - quan) % 2 == 0)
quan = 0;
else if((a[i] - quan) % 2 == 1)
quan = 1;
}
if(quan == 0)
cout << "YES";
else
cout << "NO";
}
C - 可怕的宇宙射线
一道图的题,宇宙射线会在方格纸绘制的宇宙中前进,前进n格收到指令后会向左右45°分裂。
计算在给定的分裂次数和指令时间下经过的格数。分裂过程如下图:
解析及代码
乍一看这是一道正常的深搜题,很快地写出如下代码:
#include<iostream>
using namespace std;
int dx[]={0, 1, 1, 1, 0, -1, -1, -1};
int dy[]={1, 1, 0, -1, -1, -1, 0, 1};
int ans = 0;
int n;
bool vis[400][400] = {0};
int a[30];
void dfs(int x, int y, int num, int dir) {
if (num == n)
return;
for (int i = 0; i < a[num]; i++) {
x = x + dx[dir];
y = y + dy[dir];
vis[x][y] = 1;
}
dfs(x, y, num + 1, (dir + 1) % 8);
dfs(x, y, num + 1, (dir + 7) % 8);
return;
}
int main() {
cin >> n;
for (int i = 0 ; i < n ; i++)
scanf("%d", &a[i]);
dfs(200, 200, 0, 0);
for (int i = 0 ; i < 400 ; i++)
for (int j = 0 ; j < 400 ; j++)
if (vis[i][j])
ans++;
cout << ans;
return 0;
}
简单计算一下,数据范围为n < 30 , 移动次数小于5,5 * 30 = 150 ,射线活动范围在300 * 300 之间。把数组开大一点儿,用vis[400][400] 来表示格子是否已经到达过。
这里用两个常量数组dx[ ] ,dy[ ]来确定分支的方向,从向上开始,顺时针8个方向。
int dx[]={0, 1, 1, 1, 0, -1, -1, -1};
int dy[]={1, 1, 0, -1, -1, -1, 0, 1};
之后就是常规的深搜,可是这样会在第5个点TLE。我们要想办法对其进行剪枝。经过一番查阅,最后采用了记忆性剪枝的办法。
举个例子,在深搜时的某一支路上,射线在a点进行了第n次分裂,方向为b、c,在另一条支路上,射线也可能在a点进行第n次分裂,方向为b、c。如果遇到这种情况,继续进行下去显然是没有意义的。这种情况在数据越大时越容易出现,我们想办法把这种情况剪掉。
构建一个bool数组cut [x] [y] [num] [dir] (xy为横纵坐标,num为分裂次数,dir为方向,对应上面0-7),只需在深搜时对每次分裂进行记录,如果对应的点在同一次有相同方向的分裂,那么减去,否则就标记cut数组并继续。只需在原DFS函数上加一个if else判断:
if(cut[x][y][num][dir])
return;
else
cut[x][y][num][dir] = true;
完整代码如下:
#include<iostream>
using namespace std;
int dx[] = {0, 1, 1, 1, 0, -1, -1, -1};
int dy[] = {1, 1, 0, -1, -1, -1, 0, 1};
int ans = 0;
int n;
bool vis[400][400] = {0};
int a[30];
// 新增一个四维数组,用于剪枝
bool cut[400][400][40][10] = {0};
void dfs(int x, int y, int num, int dir){
// 只需要在这里多加一段记忆性剪枝
if(cut[x][y][num][dir])
return;
else
cut[x][y][num][dir] = true;
if(num == n)
return;
for(int i = 0 ; i < a[num] ; i++) {
x = x + dx[dir];
y = y + dy[dir];
if(!vis[x][y])
{
vis[x][y] = 1;
ans++;
}
}
dfs(x, y, num + 1, (dir + 1) % 8);
dfs(x, y, num + 1, (dir + 7) % 8);
return;
}
int main(){
cin >> n;
for(int i = 0 ; i < n ; i++)
scanf("%d", &a[i]);
dfs(200, 200, 0, 0);
// for (int i = 0 ; i < 400 ; i++)
// for (int j = 0 ; j < 400 ; j++)
// if (vis[i][j])
// ans++;
cout << ans;
return 0;
}
回顾
这道题刚开始用BFS没有找到很好的方式,先留个flag,继续研究一下BFS的方法。