目录
前言
复习第十天了,虽然每天都勤勤恳恳的做题但是发现好像没有什么成长,每次感觉有些进步了去比赛结果就是草草的写完签到题后发现一个都不会......
心比天高,命比纸薄。
今天总共六道题目,三道差分,两道双指针和一道归并排序。
空调

分析
之前做过一个空调2好像是一道搜索 + 差分的题目。
题目给定我们每个牛栏的理想温度 p
和当前温度 t
。
二者作差 ,我们就得到了一个相对温度的数组,那么问题就转化为了在全为0
的数组上进行若干次区间操作使得区间转化为目标区间。
不用我说大家也知道,考察差分的本质。一次差分后统计正数或者负数的和就好。
如何来理解呢?
主播之前讲过一道类似的题目,当时主播是用前缀和影响后缀 的思路去讲解的,感兴趣的可以去看我这篇博客Day3,其中的最后一道题就涉及差分。
而今天主播要用另外一种思路来讲解差分的本质。
首先我们要明白一个知识点,差分可以视为对原数组的求导操作。
这点可以从差分可导数的表达式中看出, 即s[i] - s[i - 1]
和(y' - y)/dx
,不同的是导数是对连续的函数进行的操作,而差分是对离散的数据进行的操作。
了解了这个知识点之后我们来分析题目。
假设进行了x
次区间操作将原数组转换成了目标数组,那么每一次的区间操作都可以视为一个常函数,我们列出表达式:
s1 + s2 + s3 + ... + sx = s
我们对等号左右两边同时求导(做差分),将每次的区间操作都视为两次修改后缀的操作,即:
2 * x = a
,a为s
差分后的数组。随后我们就可以统计a
中数字的数量而求出x
。(为了保证每次都有两次修改后缀的操作,我们需要将区间扩宽1
后再进行差分操作)
代码
cpp
#include<iostream>
using namespace std;
const int N = 100010;
int p[N], t[N];
int n;
int main()
{
scanf("%d", &n);
for(int i = 1; i <= n; i++) scanf("%d", p + i);
for(int i = 1; i <= n; i++) scanf("%d", t + i);
for(int i = 1; i <= n; i++) p[i] -= t[i]; //计算相对问题,转化成全部转化为0
for(int i = n + 1; i >= 1; i--) p[i] -= p[i - 1]; // 差分
int l1 = 0, l2 = 0;
for(int i = 1; i <= n + 1; i++)
if(p[i] > 0) l1 += p[i];
printf("%d", l1); //取最大值,多出的部分其实是改变整个后缀
return 0;
}
棋盘

分析
题目很简单涉及二维前缀和,我们将每个点为偶数视为白子,奇数设为黑子,随后进行区间修改即可
代码
cpp
// 取反操作,我们根据数字的奇偶性来区分,偶数为白子,奇数为黑子
// 每次进行取反操作其实就是将区间上的每一个位置加上一,发现是离线问题,考虑二维差分
// 操作数不会大于2000, 所以用int来存储,
// 注意到空间可能较大,需要尽可能的节省空间,所以差分和前缀和操作在一个数组上进行。
#include<iostream>
using namespace std;
const int N = 2010;
int n, m;
int a[N][N];
int main()
{
scanf("%d%d", &n, &m);
while(m--)
{
int x1, x2, y1, y2;
scanf("%d%d%d%d", &x1, &y1, &x2, &y2);
a[x1][y1]++;
a[x1][y2 + 1]--;
a[x2 + 1][y1]--;
a[x2 + 1][y2 + 1]++; //差分操作
}
for(int i = 1; i <= n; i++)
for(int j = 1; j <= n; j++)
a[i][j] += a[i - 1][j] + a[i][j - 1] - a[i - 1][j - 1];
for(int i = 1; i <= n; puts(""), i++)
for(int j = 1; j <= n; j++)
printf("%d", a[i][j] & 1);
return 0;
}
重新排序

分析
分析题目我们可以发现题目要求的是整体的最大值而不是具体的方案,那么可以先从整体分析。
不去分析每次查询而去分析整体就可以发现每个位置查询到的次数不同,贪心思路就是将被查询的次数多的地方放上大数。
所以我们差分统计出每个位置被查询了多少次,随后将数组和差分数组进行排序,对应位置的数字相乘求和即可。
代码
cpp
// 总和最多可以增加多少,这显然是一个整体问题,需要整体分析
// 我们可以将每个位置被查询的次数统计出来,随后从大到小依次填补每一个位置
// 要统计每个位置被查询的次数就要用到差分,所以这题是贪心 + 差分
#include<iostream>
#include<algorithm>
using namespace std;
typedef long long LL;
const int N = 100010;
int n, m;
int A[N], st[N];
LL l1, l2, S[N];
int main()
{
scanf("%d", &n);
for(int i = 1; i <= n; i++) scanf("%d", A + i);
for(int i = 1; i <= n; i++) S[i] = S[i - 1] + A[i];
scanf("%d", &m);
while(m--)
{
int l, r;
scanf("%d%d", &l, &r);
l1 += S[r] - S[l - 1];
st[l]++; st[r + 1]--;
}
for(int i = 1; i <= n; i++) st[i] += st[i - 1];
sort(A + 1, A + n + 1);
sort(st + 1, st + n + 1);
for(int i = 1; i <= n; i++)
l2 += (LL)st[i] * A[i];
printf("%lld", l2 - l1);
return 0;
}
牛的学术圈I

分析
发现可以枚举h
求解,考虑二分。
我们对h
进行二分,每次去统计大于等于 h
的数量l1
和等于h - 1
的数量l2
。
最后判断表达式h <= l1 + min(L, l2)
即可,最终时间复杂度为O(nlogn)
,是可以通过题目的。
不过做这道题的目的是复习双指针算法,我们来思考一下双指针该怎么写。
对于双指针,主播的印象是------代码很简单,但是条件很隐形,一般需要先对数组进行排序。
所以我们先对数组进行排序,随后我们可以发现对于任意的h
,可以将数组划分为两个区间。
一边大于等于h
,一边小于h
,并且随着h
的升高或降低区间的边界是线性变化的。
所以我们就可以使用二分查找或者双指针来查找位置。
对于题目中设计的k
次引用操作我们单独判断即可。
代码
cpp
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 100010;
int n, l;
int c[N];
int main()
{
scanf("%d%d", &n, &l);
for(int i = 1; i <= n; i++) scanf("%d", c + i);
sort(c + 1, c + n + 1);
int h = n; //从最大开始枚举,最开始都是不能满足的。每次减少h,i增大
for(int i = n; ;h--)
{
while(i >= 1 && c[i] >= h) i--; //枚举位置,最开始都是不能满足的
int j = 0;
while(j < l && i >= 1 && c[i] == h - 1) i--, j++; //可扩展的部分
if(n - i >= h) //恰好满足的位置
break;
}
printf("%d", h);
return 0;
}
日志统计

分析
对时间进行排序,随后转化成滑动窗口 问题,用双指针求解。
代码
cpp
// 排序 + 双指针(滑动窗口)
#include<iostream>
#include<set>
#include<algorithm>
#define s second
#define f first
using namespace std;
typedef pair<int, int> PII;
const int N = 100010;
int n, d, k;
PII tm[N];
int st[N];
set<int> mySet;
int main()
{
scanf("%d%d%d", &n, &d, &k);
for(int i = 1; i <= n; i++) scanf("%d%d", &tm[i].f, &tm[i].s);
sort(tm + 1, tm + n + 1);
// 双指针
for(int l = 1, r = 0; l <= n; l++)
{
while(r < n && tm[r + 1].f < tm[l].f + d)
{
r++;
if(++st[tm[r].s] == k ) mySet.insert(tm[r].s);
}
st[tm[l].s]--;
}
for(int x : mySet)
printf("%d\n", x);
return 0;
}
火柴排队

分析
可以观察到,距离与顺序无关,只与相对应的二元组有关。
随后我们思考什么情况下距离最小。
嗯......想不出来,猜一个,两个数组均排序后距离最小。
证明一下判断正确性,我们假设存在最优的方案但是不满足顺序条件,即:
至少存在一个Ai < Ai+1, Bi>Bi+1
的两个二元组,我们计算出此时和Bi,Bi+1
交换后的距离,即:
(Bi+1 - Ai)^2 + (Bi - Ai+1)^2
(Bi - Ai)^2 + (Bi+1 - Ai+1)^2
二者作差,可得
-
2 * (Ai+1 - Ai) * (Bi + 1 - Bi)
因为Ai < Ai+1, Bi>Bi+1
,所以Ai+1 - Ai < 0 && Bi+1 -Bi > 0
,最终结果小于0
,与假设不服,假设不成立。
所以全部排序后对应就是一种最优解。
前面我们分析了最小距离与顺序无关,只与每对二元组有关,所以我们可以先排序求出每个二元组,随后问题就转化成了将两个数组全部匹配成对应的二元组。
为控制变量 ,我们考虑只对一个数组进行操作。那么问题就转化成了求B
对于A
的逆序对的数量。
因为数据量比较大,所以我们考虑树状数组 或者归并排序。
主播将两者的代码都写出来了。
代码
cpp
// 贪心 + 求逆序对......
#include<iostream>
#include<algorithm>
#include<unordered_map>
using namespace std;
typedef long long LL;
const int N = 100010, mod = 99999997;
LL l;
int n;
int a[N], b[N], c[N];
int tree[N]; //打表和树状数组
unordered_map<int, int> mymap, st;
int lowbit(int x)
{
return x & -x;
}
void insert(int i, int x)
{
if(i > n) return;
tree[i] += x;
insert(i + lowbit(i), x);
}
int find(int i)
{
return i == 0 ? 0 : tree[i] + find(i - lowbit(i));
}
int main()
{
scanf("%d", &n);
for(int i = 1; i <= n; i++) scanf("%d", a + i);
for(int i = 1; i <= n; c[i] = b[i], i++) scanf("%d", b + i);
for(int i = 1; i <= n; i++)
mymap[a[i]] = i; //确定原始位置
sort(a + 1, a + n + 1);
sort(b + 1, b + n + 1);
for(int i = 1; i <= n; i++)
st[b[i]] = mymap[a[i]]; //确定优先级
for(int i = 1; i <= n; i++)
c[i] = st[c[i]]; //离散化,简化后面操作
// 树状数组,计算逆序对
for(int i = 1; i <= n; i++)
{
insert(c[i], 1);
l = (l + i - find(c[i])) % mod;
}
printf("%lld", l);
return 0;
}