NOI ONLINE提高組

NOI ONLINE提高組

1. 序列 題目網址 提高

題目描述

小D有一個長度爲n的整數序列a1...na_{1...n}, 他想通過若干次操作把它變成序列bib_i
小D有m種可選的操作, 第i種操作可使用三元組(ti,ui,vi)(t_i, u_i, v_i)描述:若tit_i = 1, 則塔克以使auia_{u_i}avia_{v_i}都加一或減一;若tit_i = 2, 則她可以使auia_{u_i}減一, avia_{v_i}加一, 或者是auia_{u_i}加一, avia_{v_i}減一, 因此當ui=viu_i = v_i時, 這種操作相當於沒有操作。
小D可以以任意順序執行操作, 且每種操作都可進行無數次。現在給定序列與所有操作, 請你幫他判斷是否存在一種方案能將aia_i變成bib_i。題目保證兩個序列長度都爲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

可見, dist(a,b)dist(b,c)=dist(a,c)dist(a, b) \oplus 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 的排列pip_i, 接下來有m次操作, 操作共兩種:
1.交換操作:給定x, 把當前排列的第x個數與第x+1個數交換位置。
2.詢問操作:給定k, 請你求出當前排列經過k輪冒泡排序後逆序對的個數。對一長度爲n的排列pip_i進行一輪冒泡排序的僞代碼如下:

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數組表示在當前下標位置上, 前面有多少個比他大。
設當前交換的數爲axa_x, , bxb_x爲當前這個位置前有幾個比它大的數。
ax<ax+1a_x \lt a_{x+1}時, 那麼交換後初始逆序對個數會加一, 同時, 在x+1的位置上有多了個ax+1所以bx+1b_{x+1}也會加1。但是當bxb_x爲0時, 也就是x前沒有數比axa_x大時, 那麼這個逆序對就是無效的, 因爲下一輪冒泡接着就交換回去了。
ax>ax+1a_x \gt a_{x+1}時, bxb_x - 1.當bx+1b_{x+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的正整數序列aia_i,下標從1開始編號。我們將該序列視爲一個首尾相連的環, 對於下標爲i,j(ij)i,j(i \leq j)的兩個數ai,aja_i, a_j, 他們的距離爲min(ji,i+nj)min(j-i, i+n-j)
現在再給定m個整數k1,k2......,kmk_1, k_2......, k_m, 對每個ki(i=1,2,....,m)k_i(i = 1, 2,...., m),你需要將上面的序列aia_i重新排列, 使得換上任意兩個距離爲kik_i的數字的乘積之和最大。

題目分析

k = 0

顯然, 每個數距離爲0的點就是他自己, 所以, 答案就爲每個數的平方和。

k = 1

看樣例, 我們的6個數中要使相鄰兩個數的乘積之和最大, 那麼先把6放進去。然後再放與他相鄰最大的5,4, 再放最大的2,3, 但大貼大, 小貼小。這樣很顯然是正確的, 因爲設5, 4, 爲一組, 設成a, b; 3,2爲一組, 設成c, d;那麼我們這一種方案就是ac+bdac+bd
另一種就是ad+bcad+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;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章