NOI ONLINE提高组

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;只要两个点之间有操作, 那么只要两个边有操作, 那么就连边。可见, 只要两个点是联通的, 就能在这两个点身上进行操作。
如上面两种转移操作。

第一种转移
1
1
0
a
b
c
第二种转移
0
1
1
a
b
c

可见, 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(ij)的两个数 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(ji,i+nj)
现在再给定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;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值