NOI ONLINE提高组
1. 序列 题目网址 提高
题目描述
小D有一个长度为n的整数序列
a
1...
n
a_{1...n}
a1...n, 他想通过若干次操作把它变成序列
b
i
b_i
bi。
小D有m种可选的操作, 第i种操作可使用三元组
(
t
i
,
u
i
,
v
i
)
(t_i, u_i, v_i)
(ti,ui,vi)描述:若
t
i
t_i
ti = 1, 则塔克以使
a
u
i
a_{u_i}
aui与
a
v
i
a_{v_i}
avi都加一或减一;若
t
i
t_i
ti = 2, 则她可以使
a
u
i
a_{u_i}
aui减一,
a
v
i
a_{v_i}
avi加一, 或者是
a
u
i
a_{u_i}
aui加一,
a
v
i
a_{v_i}
avi减一, 因此当
u
i
=
v
i
u_i = v_i
ui=vi时, 这种操作相当于没有操作。
小D可以以任意顺序执行操作, 且每种操作都可进行无数次。现在给定序列与所有操作, 请你帮他判断是否存在一种方案能将
a
i
a_i
ai变成
b
i
b_i
bi。题目保证两个序列长度都为n。若方案存在输出YES, 否则输出NO.
题目思路
假设现在又a, b, c三个数。
则如果是操作1的话, 在a, b上加x, 在b, c上减去x, 就能做到在a上加x, c上减x, 一看, 正好是第二种操作。
操作二的话, 在a上+x, b上-x, b上+x, c上+x, 那么看a, c, 又是操作一!!!!!
把操作1设为边权为1, 操作2设为边权为0;只要两个点之间有操作, 那么只要两个边有操作, 那么就连边。可见, 只要两个点是联通的, 就能在这两个点身上进行操作。
如上面两种转移操作。
第一种转移
第二种转移
可见,
d
i
s
t
(
a
,
b
)
⊕
d
i
s
t
(
b
,
c
)
=
d
i
s
t
(
a
,
c
)
dist(a, b) \oplus dist(b, c) = dist(a, c)
dist(a,b)⊕dist(b,c)=dist(a,c)
那么, 就想到了并查集的操作, 每次吧中专点枚举唯根节点即可。
还有判断为一的自环的问题, 用一个数组记录, 足矣!!
代码
#include <cstdio>
#include <iostream>
#include <vector>
using namespace std;
inline long long readint(){
long long a = 0; char c = getchar(), f = 1;
for(; c<'0'||c>'9'; c=getchar())
if(c == '-') f = -f;
for(; '0'<=c&&c<='9'; c=getchar())
a = (a<<3)+(a<<1)+(c^48);
return a*f;
}
inline void writeint(long long x){
if(x < 0) putchar('-'), x = -x;
if(x > 9) writeint(x/10);
putchar((x%10)^48);
}
# define MB template < typename T >
MB void getMax(T &a,const T &b){ if(a < b) a = b; }
MB void getMin(T &a,const T &b){ if(b < a) a = b; }
const int MaxN = 100005;
int fa[MaxN], val[MaxN];
inline int findSet(int a){
if(fa[a] == a) return a;
int root = findSet(fa[a]);
val[a] ^= val[fa[a]];
return fa[a] = root;
}
bool win[MaxN]; // 是否有长度为1的自环
void unionSet(int a,int b,int c){
int x = findSet(a), y = findSet(b);
int dis = val[a]^c^val[b];
if(x == y) win[x] = win[x] or dis == 1;
else{
fa[x] = y, val[x] = dis;
win[y] = win[y] or win[x];
}
}
long long a[MaxN]; int n, m;
int main(){
// freopen("sequence.in","r",stdin);
// freopen("sequence.out","w",stdout);
for(int T=readint(); T; --T){
n = readint(), m = readint();
for(int i=1; i<=n; ++i){
a[i] = readint();
fa[i] = i, val[i] = 0;
win[i] = false;
}
for(int i=1; i<=n; ++i)
a[i] = readint()-a[i];
for(int opt,x; m; --m){
opt = readint()%2, x = readint();
unionSet(x,readint(),opt);
}
for(int i=1,rt; i<=n; ++i){
rt = findSet(i);
if(rt == i) continue;
if(val[i] == 1) // a[i]-=a[i],a[rt]-=a[i]
a[rt] -= a[i]; // 权值同时增加a[i] ...
else a[rt] += a[i]; // ... 需求便减少了
}
bool ok = true;
for(int i=1,rt; i<=n and ok; ++i){
rt = findSet(i);
if(rt != i) continue;
if(win[rt]) a[rt] %= 2;
if(a[rt] != 0) ok = false;
}
if(ok) puts("YES"); else puts("NO");
}
return 0;
}
2.冒泡排序 题目链接 提高
题目描述
给定一个1 ~ n 的排列
p
i
p_i
pi, 接下来有m次操作, 操作共两种:
1.交换操作:给定x, 把当前排列的第x个数与第x+1个数交换位置。
2.询问操作:给定k, 请你求出当前排列经过k轮冒泡排序后逆序对的个数。对一长度为n的排列
p
i
p_i
pi进行一轮冒泡排序的伪代码如下:
for i = 1 to n-1:
if p[i] > p[i+1]:
swap(p[i], p[i+1]);
题目分析
我们首先要明白冒泡排序的本质:
看一组例子:
4 1 3 2 5
3 4 2 1 5
3 2 1 4 5
2 1 3 4 5
1 2 3 4 5
每一次转移的时候, 对于当前的这一个数,只有他前面没有比它大的, 他才会转移。。。
那么假如有x个要转移的数, 一轮下来, 就减少了n-x个逆序对。
所以当前有一个数, 前面有y个数比她大的话, 他需要经过y+1轮冒泡排序才能去转移。
用树状数组去预处理没有 操作一时候冒泡排序每一轮逆序对的数量。
在考虑转移;
用a数组表示输入进去的数, b数组表示在当前下标位置上, 前面有多少个比他大。
设当前交换的数为
a
x
a_x
ax, ,
b
x
b_x
bx为当前这个位置前有几个比它大的数。
当
a
x
<
a
x
+
1
a_x \lt a_{x+1}
ax<ax+1时, 那么交换后初始逆序对个数会加一, 同时, 在x+1的位置上有多了个ax+1所以
b
x
+
1
b_{x+1}
bx+1也会加1。但是当
b
x
b_x
bx为0时, 也就是x前没有数比
a
x
a_x
ax大时, 那么这个逆序对就是无效的, 因为下一轮冒泡接着就交换回去了。
当
a
x
>
a
x
+
1
a_x \gt a_{x+1}
ax>ax+1时,
b
x
b_x
bx - 1.当
b
x
+
1
b_{x+1}
bx+1为0时, 就失效了。
差分思想查询:前缀求和。
代码
#include<bits/stdc++.h>
using namespace std;
const int maxn=2e5+5;
int n,m,a[maxn],b[maxn],d[maxn];
long long c[maxn],ans;
inline int lowbit(int x){
return x&(-x);
}
inline void update(int x,long long val){
while(x<=n){
c[x]+=val;
x+=lowbit(x);
}
}
inline long long getsum(int x){
long long res=0;
while(x>0){
res+=c[x];
x-=lowbit(x);
}
return res;
}
int main(){
int opt,x,tmp=0;
scanf("%d%d",&n,&m);
for(int i=1;i<=n;++i){
scanf("%d",&a[i]);
b[i]=i-1-getsum(a[i]);
ans+=b[i],++d[b[i]];
update(a[i],1);
}
memset(c,0,sizeof(c));
update(1,ans);
for(int i=0;i<n;++i){
tmp+=d[i];
update(i+2,-(n-tmp));
}
for(int i=1;i<=m;++i){
scanf("%d%d",&opt,&x);
x=min(x,n-1);
if(opt==1){
if(a[x]<a[x+1]){
swap(a[x],a[x+1]);
swap(b[x],b[x+1]);
update(1,1);
update(b[x+1]+2,-1);
b[x+1]++;
}
else{
swap(a[x],a[x+1]);
swap(b[x],b[x+1]);
update(1,-1);
b[x]--;
update(b[x]+2,1);
}
}
else printf("%lld\n",getsum(x+1));
}
return 0;
}
3.最小环 题目描述 提高
题目描述
给定一个长度为n的正整数序列
a
i
a_i
ai,下标从1开始编号。我们将该序列视为一个首尾相连的环, 对于下标为
i
,
j
(
i
≤
j
)
i,j(i \leq j)
i,j(i≤j)的两个数
a
i
,
a
j
a_i, a_j
ai,aj, 他们的距离为
m
i
n
(
j
−
i
,
i
+
n
−
j
)
min(j-i, i+n-j)
min(j−i,i+n−j)。
现在再给定m个整数
k
1
,
k
2
.
.
.
.
.
.
,
k
m
k_1, k_2......, k_m
k1,k2......,km, 对每个
k
i
(
i
=
1
,
2
,
.
.
.
.
,
m
)
k_i(i = 1, 2,...., m)
ki(i=1,2,....,m),你需要将上面的序列
a
i
a_i
ai重新排列, 使得换上任意两个距离为
k
i
k_i
ki的数字的乘积之和最大。
题目分析
k = 0
显然, 每个数距离为0的点就是他自己, 所以, 答案就为每个数的平方和。
k = 1
看样例, 我们的6个数中要使相邻两个数的乘积之和最大, 那么先把6放进去。然后再放与他相邻最大的5,4, 再放最大的2,3, 但大贴大, 小贴小。这样很显然是正确的, 因为设5, 4, 为一组, 设成a, b; 3,2为一组, 设成c, d;那么我们这一种方案就是
a
c
+
b
d
ac+bd
ac+bd
另一种就是
a
d
+
b
c
ad+bc
ad+bc, 显然, 两者一减, 前一种方案大。
k = 2
把他分成两个环, 分别解决。
多了
环的长度为n/gcd(n, k)
长度一样答案一样, 记得记忆化。
#include <bits/stdc++.h>
using namespace std;
map<int, long long> record;
int gcd(int a, int b)
{
int temp;
while (b)
{
temp = b;
b = a % b;
a = temp;
}
return a;
}(
long long a[200005];
int main()
{
int n, m;
scanf("%d%d", &n, &m);
for (int i = 0; i < n; i++)
scanf("%lld", a + i);
sort(a, a + n, greater<long long>()); //从大到小排序
for (int i = 0; i < m; i++)
{
int k;
scanf("%d", &k);
long long answer = 0;
if (k == 0) //特判,不然下面gcd会出错
{
for (int p = 0; p < n; p++)
answer += a[p] * a[p];
printf("%lld\n", answer);
continue;
}
int ring = n / gcd(n, k); //环长
if (record[ring]) //记忆化
{
printf("%lld\n", record[ring]);
continue;
}
for (int p = 0; p < n; p += ring)
{ //对于每一个环,p记录每个环最开始的点的下标
for (int x = 0, tp = p + 1; x < (ring - 2) / 2; x++, tp += 2) //一半环
answer += a[tp] * a[tp + 2];
for (int x = 0, tp = p; x < (ring - 1) / 2; x++, tp += 2) //另一半环
answer += a[tp] * a[tp + 2];
answer += a[p] * a[p + 1] + a[p + ring - 1] * a[p + ring - 2]; //最后处理两个半环链接的问题
}
printf("%lld\n", answer);
record[ring] = answer; //记录
}
return 0;
}