第十四周作业
目录
- MT2134 泡泡
- MT2135 调整队伍
- MT2141 快排变形
- MT2142 逆序
- MT2143 线段树
MT2134 泡泡
难度:黄金 时间限制:1秒 占用内存:128M
题目描述
小码哥小时候喜欢吹泡泡,有一次他吹出了 n n n 个一样小的泡泡,过了一段时间后,这些泡泡相互碰到一起,形成了各种大小的泡泡…,但小码哥不知道的是,他生活的地方是母体构筑的虚拟世界,里面的泡泡实际是一串串被拼成环状的数字。
小码哥一开始吹出的泡泡被母体记为 1 , 2 , . . . , n 1,2,...,n 1,2,...,n,而泡泡的碰撞融合实际是数字的拼接(有序)。母体会通过模拟得知两个泡泡环碰撞的情况(用 x → y x→y x→y 表示)。例如,有一个为 1 − 2 1-2 1−2 的泡泡环与 3 − 4 − 5 3-4-5 3−4−5 的泡泡环碰撞,碰撞的点为 1 → 4 1→4 1→4 (后一个数字接在前一个数字下面〉,则会形成 1 − 4 − 5 − 3 − 2 1-4-5-3-2 1−4−5−3−2 的泡泡环。
一开始所有泡泡环都只有一个数字,母体演算出了泡泡之后的碰撞点,现在请你输出泡泡碰撞完后的所有泡泡的情况。格式
输入格式:第一行两个整数 n , m n,\ m n, m,表示一开始泡泡的数量和泡泡碰撞的次数;
接下来 m m m 行,每行两个数字 x , y x,y x,y,表示泡泡碰撞的两个点。
输出格式:输出所有泡泡的情况,一行表示一个泡泡的情况(要求按字典序最小的方式按顺序输出)。样例 1
输入:9 6
4 1
6 1
5 1
6 8
2 4
9 7输出:1 2 4 6 8 5
3
7 9备注
其中: 1 ≤ m < n ≤ 10 6 , 1 ≤ x ≤ y ≤ n 1\le m<n\le{10}^6,\ 1\le x\le y\le n 1≤m<n≤106, 1≤x≤y≤n,保证 x , y x,\ y x, y 属于不同的泡泡环。
相关知识点:
链表
题解
考察了双向链表,但这道题的描述感觉不够形象,理解题目都有一定难度。不过整体也不是很难,本质就是链表内部断链与重连的过程。这道题有一个技巧,就是可以利用数组的下标作为泡泡的 valve 值,这样能省去检索时间从而提高访问效率。下面直接给出完整代码:
/*
MT2134 泡泡
*/
#include<bits/stdc++.h>
using namespace std;
const int MAX = 1e6+5;
struct Node{
// 当前对象的前、后向指针
int l,r;
bool flag;
};
Node node[MAX];
int main( )
{
int n, m, x, y, tmpl, tmpr, tmp;
cin>>n>>m;
for(int i=1;i<=n;i++)
node[i].l=node[i].r=i;
while(m--){
cin>>x>>y;
tmpl = node[x].r, tmpr = node[y].l;
node[x].r = y, node[y].l = x;
node[tmpl].l = tmpr, node[tmpr].r = tmpl;
}
for(int i=1;i<=n;i++){
if(!node[i].flag){
tmp = i;
while(!node[tmp].flag){
cout<<tmp<<" ";
node[tmp].flag = true;
tmp = node[tmp].r;
}
cout<<endl;
}
}
return 0;
}
MT2135 调整队伍
难度:黄金 时间限制:1秒 占用内存:128M
题目描述
小码哥迎来了他大学的第一次军训,军训的第一个项目就是列队,每个同学在队伍中都有属于自己的编号。但在练习过程中,教官怎么看这支队伍怎么不顺眼,于是决定调整一下队伍的顺序。
为了顺便考验同学们的反应力,教官与学生们约定了一个口令,每当他说x y 0
,x 号同学就要移动到 y 号同学的前面,每当他说x y 1
,x 号同学就要移动到 y 号同学的后面,但学生们不想一次次慢慢移动这么麻烦,想要知道队伍最后是怎么排的然后直接排好。为了解决同学们的烦恼,小码哥立刻动手原地写了一个程序算出了最终排序。
现在告诉你队伍从前到后的初始排序编号以及教官的口令,要你得出队伍从前往后的最终排序。已知有 n 个学生,编号不重复且 1 ≤ 编号 ≤ n 。格式
输入格式:第一行包含两个整数 n , m n,\ m n, m,分别表示学生的数量和教官口令的数量;
第二行 n n n 个正整数,表示学生们的初始排序;
接下来 m m m 行,每行 3 个整数,表示教官的口令。
输出格式: n n n 个数字,每个数字之间用空格隔开,表示队伍从前往后的最终排序。样例1
输入:10 5
8 4 6 7 9 3 5 10 1 2
4 9 0
10 5 1
3 9 0
1 9 1
3 6 0输出:8 3 6 7 4 9 1 5 10 2
备注
其中: 2 ≤ n ≤ 3 × 10 5 , 1 ≤ m ≤ 3 × 10 6 2\le n\le3\times{10}^5,\ 1\le m\le3\times{10}^6 2≤n≤3×105, 1≤m≤3×106,保证 1 ≤ x ≤ y ≤ n 1\le x\le y\le n 1≤x≤y≤n 且 x ≠ y x\neq y x=y。
相关知识点:
链表
题解
本题依然是考察双向链,下面直接给出完整代码:
/*
MT2135 调整队伍
*/
#include<bits/stdc++.h>
using namespace std;
const int MAX = 3e5+5;
struct Node{
// 当前对象的前、后向指针
int l,r;
};
Node node[MAX];
int n, m, head, now;
int main( )
{
int tmp, x, y ,opt, tmpl, tmpr;
cin>>n>>m;
while(n--){
cin>>tmp;
node[tmp].l = now;
node[now].r = tmp;
now = tmp;
}
while(m--){
cin>>x>>y>>opt;
tmpl = node[x].l, tmpr = node[x].r;
node[tmpl].r = tmpr, node[tmpr].l = tmpl;
tmpl = node[y].l, tmpr = node[y].r;
if(opt){
node[x].r = tmpr;
node[x].l = y;
node[y].r = x;
node[tmpr].l = x;
}else{
node[x].l = tmpl;
node[x].r = y;
node[y].l = x;
node[tmpl].r = x;
}
}
now = node[head].r;
while(now){
cout<<now<<" ";
now = node[now].r;
}
return 0;
}
MT2141 快排变形
难度:黄金 时间限制:1秒 占用内存:128M
题目描述
有 n n n 个数,求通过每次交换邻项的两个数来使数组中的元素变为升序排列的最小交换次数。如序列 {9, 8, 7, 6, 5} 变为升序 {5, 6, 7, 8, 9} 最少需要 10 次邻项交换。
格式
输入格式:第一行输入一个整数 n n n ,表示序列长度;
第二行输入 n n n 个数;。
输出格式:输出一个整数,表示最少的交换次数。样例 1
输入:5
9 8 7 6 5输出:10
备注
其中: 1 ≤ n ≤ 200000 1\le n\le200000 1≤n≤200000。
相关知识点:
树状数组
题解
这道题实际上考察的是求序列的逆序对个数。
若要用最少的邻项交换次数使得该序列变为升序,那么我们每次交换就一定要减少整个序列的逆序对个数(进行有效交换)。在这样的情况下,如果我们交换了 n 次,就表示该序列有 n 个逆序对。例如题目给出的例子中(序列 {9, 8, 7, 6, 5} )含有的逆序对为:{<9, 8>, <9, 7>, <9, 6>, <9, 5>, <8, 7>, <8, 6>, <8, 5>, <7, 6>, <7, 5>, <6, 5> }共10 个,因此其通过邻项交换使原序列变得有序的最低交换次数就为10。
求逆序对可以用归并排序也可以用树状数组(实际上,求逆序对是树状数组的一个经典用例),由于前面的作业已经用过归并排序求逆序对,因此下面给出利用树状数组求解的方法(专栏链接:【算法与数据结构】—— 树状数组,该文对树状数组的算法原理进行了详细分析和讲解,并在最后附上了利用树状数组求解逆序对的算法),此处直接给出求解本题的完整代码(已 AC):
/*
MT2141 快排变形
*/
#include <bits/stdc++.h>
using namespace std;
const int MAX = 2e5 + 5;
// a 数组存放数据序号、b 数组存放数据绝对取值
int a[MAX], b[MAX], c[MAX], n;
long ans;
// 树状数组模板
int lowbit(int x) { return x & -x; }
void add(int i, int x)
{
while(i<=n){
c[i] += x;
i += lowbit(i);
}
}
int sum(int i)
{
int ans = 0;
while(i>0){
ans += c[i];
i -= lowbit(i);
}
return ans;
}
// 进行数据压缩时的比较函数
bool cmp(const int x, const int y)
{
if (b[x] == b[y])
return x > y;
return b[x] > b[y];
}
MT2142 逆序
难度:钻石 时间限制:1秒 占用内存:128M
题目描述
给一个长度为 n n n 的排列 p p p,找一个元素,使得从排列中取出这个元素以后排列的|
records
最多。一个record
是一个元素 a i a_i ai 满足:对于每个正整数 1 ≤ j < i , a i > a j 1\le j<i,\ a_i>a_j 1≤j<i, ai>aj。格式
输入格式:第一行为 n n n;
第二行为排列 p p p。
输出格式:一个整数即答案。样例 1
输入:5
5 1 2 3 4输出:5
备注
其中: 1 ≤ n , a i ≤ 1 e 5 1\le n,\ \ a_i\le1e5 1≤n, ai≤1e5。
当答案不唯一时,输出较小的数。如:
(1) 3 4 5 1 2
(2) 3 4 5 2 1
上面两种情况,均输出1。
相关知识点:
树状数组
题解
首先要弄清楚题目所说的 record
是什么意思。实际上,一个 record 是指在该序列中,某个元素
a
i
(
i
≥
1
)
a_i\ (i\geq1)
ai (i≥1),对任意
a
j
(
1
≤
j
≤
i
)
a_j\ (1\le j\le i)
aj (1≤j≤i) 都有
a
j
<
a
i
a_j<a_i
aj<ai 成立。每出现一个这样的
a
i
a_i
ai,就认为当前序列出现了一个 record 。例如:对序列 {1, 2, 3, 4, 5} 而言,其为一个严格递增序列,所以它的 records
为 5。对于一个长度为
n
n
n 的序列,如果我们从中 “拿走” 某个元素,其长度就变为
n
−
1
n-1
n−1,在这样的情况下,其 records 的最大取值即为
n
−
1
n-1
n−1。本题要求的是:“拿走哪个元素能使得 records 的取值最大?”对于序列 {1, 2, 3, 4, 5} ,无论我们取出其中的哪个元素,其始终保持递增,因此其最大的 records 始终为 4,但根据题目的意思(当答案不唯一时,输出较小的数),我们应该取出的数为 1。对于序列 {5, 1, 2, 3, 4},只有取出数 5,才能使得该序列的 records 最大(取其他任意数得到的 records 均为 0 )。
那要怎么找出这个数呢?一种直接的办法是:对序列中的每个元素都进行枚举,求取出该数后,整个序列的 records,并在最后输出最大 records 对应的取数。这样做法的时间复杂度为 O ( n 3 ) O\left(n^3\right) O(n3)。其中,枚举每个元素需要 O ( n ) O\left(n\right) O(n) ,对每一次枚举计算整个序列的 records 需要 O ( n 2 ) O\left(n^2\right) O(n2)。显然,这样的办法是及其低效且超时无疑的。
有没有什么办法可以避免去求 “取出某个元素后整个序列的records” ?我们可以借助动态规划的思想:定义一个数组 chg[i],表示取出元素i后,其会对整个序列带来的 records 相对增益。例如,chg[5] = 8 表示取出元素 5 后,整个序列的 records 最少会增加 8;chg[6] = -3 表示取出元素 6 后,整个序列的 records 会减少 3。接下来从头到尾对整个数组进行扫描,以完善chg[] 数组的取值。最终,整个 records 数组中的最大取值即指示了 “取出哪个元素能使得 records 的取值最大”。
chg[] 数组的状态转移规则是怎样的呢?对于某个序列,考虑当前第
i
i
i 个取值
n
u
m
num
num:
- 如果前面有 i − 1 i-1 i−1 个数比 n u m num num 小,就说明当前第 i i i 个数是目前序列(截至第 i i i 位)的最大值。也就是说, n u m num num 的存在会使得序列的records增加。因此,将num取出会降低 records,即在这种情况下 chg[num]–;
- 如果前面有 i − 2 i-2 i−2 个数比 n u m num num 小,就说明在第 i i i 个数之前存在一个比其更大的数(假设该数为 m a x n u m maxnum maxnum)。此时若删除 m a x n u m maxnum maxnum,其就就会给当前序列的 records 带来正增益,即在这种情况下 chg[maxnum]++;
- 如果前面有低于 i − 2 i-2 i−2 个数比 n u m num num 小(即存在两个及以上的数比 n u m num num 大),那就不需要考虑了,因为题目要求只能取出一个数,如果前面更大数的个数超过 1,则无论取什么数,都不会在后续形成 record,如序列 {6, 5, 1, 2, 3, 4}。因此对这种情况,直接跳过即可。
根据这样的规则,在遍历整个输入序列并完善 chg[ ] 数组后,我们就能通过寻找其中的最大值来得到最终的取数。例如,对于输入序列 {1, 2, 5, 3, 4},chg[]数组的构建过程如下(初始情况下全部元素赋0值):
- 第1个数1,其前面不存在比他大的数,因此 chg[1]–,得到 chg[1] = -1;
- 第2个数2,其前面不存在比他大的数,因此 chg[2]–,得到 chg[4] = -1;
- 第3个数5,其前面不存在比他大的数,因此 chg[5]–,得到 chg[5] = -1;
- 第4个数3,其前面存在1个比他大的数5,因此 chg[5]++,得到 chg[5] = 0;
- 第5个数4,其前面存在1个比他大的数5,因此 chg[5]++,得到 chg[5] = 1。
最后遍历整个 chg[ ] 数组,发现 chg[i] 的最大值为 chg[5] = 1,故认为取出元素 5 能使得这个序列的 records 最大。观察该数组,发现从该序列中取出元素 5 后,整个序列的 records 为4(达到了最大值),其并不一定等于 chg[5] = 1。这表明,chg[ ] 的取值反应的是 chg[ ] 中各数在被取出时对整个序列的 records 的相对大小,而不是绝对取值。
因此,根据上面的分析,可以将当前的任务转换为:在输入数据的同时(状态转移过程中),快速求出截至当前序列的非逆序数。另一方面,由于题中说明了输入序列是一个 “排列”,因此该序列中不会出现重复元素。所以我们要做的,实际上是动态计算当前序列所包含的 “顺序数”。
动态求顺序数,这很容易让我们想到一个数据结构:树状数组。这一数据结构直接套模板即可。
下面直接给出利用树状数组求解本题的完整代码:
/*
MT2142 逆序
*/
#include<bits/stdc++.h>
using namespace std;
const int MAX = 1e5+5;
int c[MAX], chg[MAX];
// 树状数组模板
int lowbit(int x)
{return x & -x;}
void update(int x){
while(x<MAX){
c[x]++;
x += lowbit(x);
}
}
int getSum(int x){
int ans = 0;
while(x>0){
ans += c[x];
x -= lowbit(x);
}
return ans;
}
int main( )
{
// 录入数据
int n, x, maxx=0, cnt, ans = 1;
cin>>n;
for(int i=1;i<=n;i++){
cin>>x;
maxx = max(maxx, x);
// 计算前面有多少个数比 x 小
cnt = getSum(x);
// 如果个数为 i-1 就表示当前最大值就是这个数本身
if(cnt == i-1) chg[maxx]--;
// 如果个数为 i-2 就表示前面存在一个比当前数更大的数
if(cnt == i-2) chg[maxx]++;
// 更新点至树状数组
update(x);
}
// 寻找 chg 数组的最大取值
for(int i=1; i<=n; i++)
if(chg[i] > chg[ans])
ans = i;
cout<<ans<<endl;
return 0;
}
MT2143 线段树
难度:钻石 时间限制:1秒 占用内存:128M
题目描述
线段树模板题。已知一个数列,你需要进行下面两种操作:
- 将某区间每一个数加上 k k k;
- 求出某区间每一个数的和。
格式
输入格式:第一行包含两个整数 n , m ( 5 ≤ n , m ≤ 10 5 ) n,\ m\ (5\le n,m\le{10}^5) n, m (5≤n,m≤105),分别表示该数列数字的个数和操作的总个数;
第二行包含 n n n 个用空格分隔的整数 ( 0 − 10 9 ) (0-{10}^9) (0−109),其中第 i i i 个数字表示数列第 i i i 项的初始值;
接下来 m m m 行每行包含 3 或 4 个整数,表示一个操作,具体如下:
1 x y k
:将区间 [x, y] 内每个数加上k。
2 x y
:输出区间 [x, y] 内的数的和。
输出格式:输出包含若干行整数,即为所有操作 2 的结果。样例 1
输入:5 5
1 5 4 2 3
2 2 4
1 2 3 2
2 3 4
1 1 5 1
2 1 4输出:11
8
20备注
其中: 1 ≤ x ≤ y ≤ n , 0 ≤ k ≤ 10 5 1\le x\le y\le n,\ 0\le k\le{10}^5 1≤x≤y≤n, 0≤k≤105。
相关知识点:
线段树
题解
这是一道纯模板题,下面直接给出完整代码:
/*
MT2143 线段树
*/
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 7;
#define int long long // 有时候觉得ll麻烦,就可以用。但记得改signed main
struct NODE
{
int l, r, val, lz; // lz为懒标记
} tree[4 * N];
int a[N];
void build(int p, int l, int r)
{
tree[p].l = l, tree[p].r = r;
if (l == r)
{
tree[p].val = a[l];
return;
}
int mid = l + ((r - l) >> 1);
build(p * 2, l, mid);
build(p * 2 + 1, mid + 1, r);
tree[p].val = tree[p * 2].val + tree[p * 2 + 1].val;
}
void lazy(int p, int v)
{
int s = tree[p].l, t = tree[p].r;
tree[p].val += (t - s + 1) * v, tree[p].lz += v;
}
void pushdown(int p)
{
lazy(2 * p, tree[p].lz);
lazy(2 * p + 1, tree[p].lz);
tree[p].lz = 0;
}
// 带懒标记区间修改,[1,r]
// 为修改区间,p为当前节点编号,c为区间节点变化值,求和非求最值
void update(int l, int r, int c, int p)
{
int s = tree[p].l, t = tree[p].r;
if (l <= s && t <= r)
return lazy(p, c);
if (tree[p].lz && s != t)
pushdown(p);
int mid = s + ((t - s) >> 1);
if (l <= mid)
update(l, r, c, p * 2);
if (r > mid)
update(l, r, c, p * 2 + 1);
tree[p].val = tree[p * 2].val + tree[p * 2 + 1].val;
}
// 带懒标记区间查询(区间求和),[L,r] 为修改区间,p为当前节点编号
int query(int l, int r, int p)
{
int s = tree[p].l, t = tree[p].r;
if (l <= s && t <= r)
return tree[p].val;
if (tree[p].lz)
pushdown(p);
int mid = s + ((t - s) >> 1), sum = 0;
if (l <= mid)
sum = query(l, r, p * 2);
if (r > mid)
sum += query(l, r, p * 2 + 1);
return sum;
}