2022 年牛客多校第八场补题记录

A Puzzle: X-Sums Sudoku

题意:考虑一宫大小为 2 n × 2 m 2^n\times 2^m 2n×2m 的方形数独, 求横排字典序最小( 4 × 2 4\times 2 4×2 的数独如下)的数度中第 x x x 行或列的前或后 X X X 个数的和,其中 X X X 为第 x x x 行或列的第一个数字。

在这里插入图片描述

其中第二行 8 = 3 + 4 + 1 8=3+4+1 8=3+4+1,因为第一个数字为 3 3 3 代表取 3 3 3 个数字。第二列最下方的 34 34 34 表示取本列后 7 7 7 个数字的和。 n , m ≤ 30 n,m \leq 30 n,m30 T T T 测, T ≤ 1 × 1 0 5 T \leq 1\times 10^5 T1×105

解法:可以参考以下的打表代码:

#include <bits/stdc++.h>
using namespace std;
const int N = 1 << 7;
bool viscol[N][N*N], visrow[N][N*N], vispalace[N][N*N];
int ans[N * N][N * N];
int main()
{
    int n, m;
    scanf("%d%d", &n, &m);
    n = 1 << n;
    m = 1 << m;
    for (int i = 0; i < n * n * m * m;i++)
    {
        int row = i / (n * m), col = i % (n * m);
        int palace_row = row / n, palace_col = col / m;
        int palace_id = palace_row * n + palace_col;
        for (int j = 0; j < n * m; j++)
            if(!viscol[col][j] && !visrow[row][j] && !vispalace[palace_id][j])
            {
                viscol[col][j] = visrow[row][j] = vispalace[palace_id][j] = 1;
                ans[row][col] = j;
                break;
            }
    }//以上为暴力
    for (int i = 0; i < n * m;i++)
    {
        if(i == 0)
        {
            printf("+");
            for (int j = 0; j < n * m;j++)
                printf("--%c", "-+"[j % m == m - 1]);
            printf("\n");
        }
        printf("|");
        for (int j = 0; j < n * m;j++)
        {
            assert(((i / n) ^ j ^ (i % n * m)) == ans[i][j]);//O(1)解法
            printf("%02X%c", (i / n) ^ j ^ (i % n * m), " |"[j % m == m - 1]);
        }
        printf("\n");
        if (i % n == n - 1)
        {
            printf("+");
            for (int j = 0; j < n * m;j++)
                printf("--%c", "-+"[j % m == m - 1]);
            printf("\n");
        }
    }
    return 0;
}

通过打表或者观察样例可以得到,若将全部数字减一,并且下标均为 0-base,则第 i i i 行第 j j j 列的数字为 ⌊ i 2 n ⌋ ⊕ j ⊕ 2 m ( i   m o d   2 n ) \displaystyle \left \lfloor \dfrac{i}{2^n}\right \rfloor \oplus j \oplus 2^m(i \bmod 2^n) 2nij2m(imod2n)。其中, ⌊ i 2 n ⌋ \left \lfloor \dfrac{i}{2^n}\right \rfloor 2ni 表示了横行宫的贡献, j j j 为列贡献, 2 m ( i   m o d   2 n ) 2^m(i \bmod 2^n) 2m(imod2n) 为宫内行贡献。同时容易注意到,整个数独是中心对称的。因而如果要算第 x x x 列从下往上的答案,可以转化到第 2 n + m − x + 1 2^{n+m}-x+1 2n+mx+1 列的答案,从右往左同理。

对于横行, X = ⌊ x 2 n ⌋ ⊕ 2 m ( x   m o d   2 n ) X=\left \lfloor \dfrac{x}{2^n}\right \rfloor \oplus 2^m(x \bmod 2^n) X=2nx2m(xmod2n)。其答案为 X + 1 + ∑ i = 0 X ( ⌊ x 2 n ⌋ ⊕ 2 m ( x   m o d   2 n ) ) ⊕ i \displaystyle X+1+\sum_{i=0}^X\left(\left \lfloor \dfrac{x}{2^n}\right \rfloor \oplus 2^m(x \bmod 2^n)\right) \oplus i X+1+i=0X(2nx2m(xmod2n))i。对于此类 ∑ i i ⊕ x \sum_{i}i \oplus x iix,其中 x x x 为一定值的,可以分位考虑,考虑第 j j j 位为 1 1 1 0 0 0 的个数。若 x x x 的第 j j j 位为 0 0 0 则计入 1 1 1 的个数,否则计入 0 0 0 的个数。

对于纵列, X = x X=x X=x,其答案为 X + 1 + ∑ i = 0 X ( ⌊ i 2 n ⌋ ⊕ 2 m ( i   m o d   2 n ) ) ⊕ X \displaystyle X+1+\sum_{i=0}^X\left(\left \lfloor \dfrac{i}{2^n}\right \rfloor \oplus 2^m(i \bmod 2^n)\right) \oplus X X+1+i=0X(2ni2m(imod2n))X。不难发现, ⌊ i 2 n ⌋ ∈ [ 0 , 2 m − 1 ] \left \lfloor \dfrac{i}{2^n}\right \rfloor \in [0,2^m-1] 2ni[0,2m1],而 2 m ( i   m o d   2 n ) 2^m (i \bmod 2^n) 2m(imod2n) 对答案的贡献一定在第 m m m 个二进制位之上。因而枚举到第 j j j 位时,需要根据当前位置进行平移——当 j ≥ m j \geq m jm 时计算的时 i   m o d   2 n i \bmod 2^n imod2n,而 j < m j<m j<m 计算的为 ⌊ i 2 n ⌋ \left \lfloor \dfrac{i}{2^n}\right \rfloor 2ni

因而单次询问复杂度为 O ( n + m ) \mathcal O(n+m) O(n+m)

#include <bits/stdc++.h>
using namespace std;
void print(__int128_t x)
{
    if(!x)
    {
        printf("0\n");
        return;
    }
    string ans = "";
    while(x)
    {
        ans += x % 10 + 48;
        x /= 10;
    }
    reverse(ans.begin(), ans.end());
    printf("%s\n", ans.c_str());
}
long long count(long long n, int digit)
{
    long long block = n >> (digit + 1), res = n - (block << (digit + 1));
    return (block << digit) + max(res - (1ll << digit), 0ll);
}
char buf[40];
int main()
{
    int t, n, m;
    long long x;
    scanf("%d", &t);
    while(t--)
    {
        scanf("%d%d%s%lld", &n, &m, buf, &x);
        x--;
        if (buf[0] == 'b')
        {
            buf[0] = 't';
            x = (1ll << (n + m)) - x - 1;
        }
        else if (buf[0] == 'r')
        {
            buf[0] = 'l';
            x = (1ll << (n + m)) - x - 1;
        }
        if (buf[0] == 'l')
        {
            x = (x >> n) ^ ((x - ((x >> n) << n)) << m);
            __int128_t ans = x + 1;
            for (int k = 0; k < n + m;k++)
            {
                __int128_t now = count(x + 1, k);
                if (x >> k & 1)
                    now = x + 1 - now;
                ans += now << k;
            }
            print(ans);
        }
        else
        {
            __int128_t ans = x + 1;
            for (int k = 0; k < n + m;k++)
            {
                __int128_t now = count(x + 1, k < m ? n + k : k - m);
                if (x >> k & 1)
                    now = x + 1 - now;
                ans += now << k;
            }
            print(ans);
        }
    }
    return 0;
}

D Poker Game: Decision

题意:桌面上有 6 6 6 张扑克牌,Alice 和 Bob 手上各有 2 2 2 张且明牌。二人以最优决策依次从桌上抽取一张牌直到二人各有五张牌,最终根据德州扑克的规则比大小。问最终谁会赢。

解法:首先写清楚德州扑克的大小比较。由于此处张数较少,可以暴力枚举每一种抽牌情况,暴力 dfs 计算当前的最优决策。设 dfs 返回值为 − 1 , 0 , 1 -1,0,1 1,0,1 为 Bob 胜利、平局、Alice 胜利,Alice 会从中选择尽可能大的状态转移,而 Bob 会选择最小的。

#pragma GCC optimize(3)
#include<bits/stdc++.h>
#define IL inline
#define LL long long
#define pb push_back
#define abs(x,y) (x<y?y-x:x-y)
using namespace std;
const int N=5e4+3,M=5e4+3;
struct kk{
	int op,r;
	bool operator<(const kk &a) const{
		return r>a.r;
	}
}a[3],b[3],c[10];
struct hh{//比较牌的大小
	int rk;kk a[6];
	int operator<(const hh &b) const{
		if(rk^b.rk) return rk<b.rk;
		for(int i=1;i<=5;++i)
		  if(a[i].r^b.a[i].r) return a[i].r<b.a[i].r;
		return 2;
	}
	void get_rk(){
		sort(a+1,a+6);kk b[6];
		int f1=1,f2=1;
		for(int i=2;i<=5;++i) if(a[i].op^a[i-1].op) f1=0;
		for(int i=2;i<=5;++i) if(a[i].r^a[i-1].r-1) f2=0;
		if(f1&&f2&&a[1].r==13) rk=12;
		else if(f1&&f2) rk=11;
		else if(f1&&a[1].r==13&&a[2].r==4&&a[3].r==3&&a[4].r==2&&a[5].r==1) rk=10;
		else if(a[1].r==a[2].r&&a[2].r==a[3].r&&a[3].r==a[4].r) rk=9;
		else if(a[2].r==a[3].r&&a[3].r==a[4].r&&a[4].r==a[5].r) rk=9,swap(a[1],a[5]);
		else if(a[1].r==a[2].r&&a[2].r==a[3].r&&a[4].r==a[5].r) rk=8;
		else if(a[1].r==a[2].r&&a[3].r==a[4].r&&a[4].r==a[5].r) rk=8,swap(a[1],a[5]),swap(a[2],a[4]);
		else if(f1) rk=7;
		else if(f2) rk=6;
		else if(a[1].r==13&&a[2].r==4&&a[3].r==3&&a[4].r==2&&a[5].r==1) rk=5;
		else if(a[1].r==a[2].r&&a[2].r==a[3].r) rk=4;
		else if(a[2].r==a[3].r&&a[3].r==a[4].r) rk=4,swap(a[1],a[2]),swap(a[2],a[3]),swap(a[3],a[4]);
		else if(a[3].r==a[4].r&&a[4].r==a[5].r){
			rk=4;for(int i=1;i<=5;++i) b[i]=a[i];
			a[1]=b[3],a[2]=b[4],a[3]=b[5],a[4]=b[1],a[5]=b[2];
		}
		else if(a[1].r==a[2].r&&a[3].r==a[4].r) rk=3;
		else if(a[1].r==a[2].r&&a[4].r==a[5].r) rk=3,swap(a[3],a[5]);
		else if(a[2].r==a[3].r&&a[4].r==a[5].r){
			rk=3;for(int i=1;i<5;++i) swap(a[i],a[i+1]);
		}
		else if(a[1].r==a[2].r) rk=2;
		else if(a[2].r==a[3].r) rk=2,swap(a[1],a[3]);
		else if(a[3].r==a[4].r) rk=2,swap(a[1],a[3]),swap(a[2],a[4]);
		else if(a[4].r==a[5].r){
			rk=2;for(int i=1;i<=5;++i) b[i]=a[i];
			a[1]=b[4],a[2]=b[5],a[3]=b[1],a[4]=b[2],a[5]=b[3];
		}
		else rk=1;
	}
}A,B,na,nb;
int cn,to[129];
IL int in(){
	char c;int f=1;
	while((c=getchar())<'0'||c>'9')
	  if(c=='-') f=-1;
	int x=c-'0';
	while((c=getchar())>='0'&&c<='9')
	  x=x*10+c-'0';
	return x*f;
}
IL void get(kk &a){
	char s[4];scanf("%s",s+1);
	a.op=to[s[2]],a.r=to[s[1]];
}
int bo[10],mp[800],pm[100];
IL int cmp(hh a,hh b){
	a.get_rk(),b.get_rk();
	int x=b<a;
	if(!x) return 0;
	if(x==1) return 2;
	return 1;
}
int dfs(int pos,int val){//暴力搜索
	if(~mp[val]) return mp[val]; 
	if(pos==7) return mp[val]=cmp(na,nb);
	mp[val]=pos&1?0:2;
	for(int i=1;i<=6;++i)
	  if(!bo[i]){
	  	int now=val;
	  	if(pos&1) na.a[(pos+1>>1)+2]=c[i],now+=pm[i-1];
	  	else nb.a[(pos>>1)+2]=c[i],now+=pm[i-1]*2;
	  	bo[i]=1;
	  	int op=dfs(pos+1,now);
	  	bo[i]=0;
	  	if(pos&1) mp[val]=max(mp[val],op);
	  	else mp[val]=min(mp[val],op);
	  }
	return mp[val];
}
IL void solve(){
	na.rk=nb.rk=0;memset(mp,-1,sizeof(mp));
	get(na.a[1]),get(na.a[2]),get(nb.a[1]),get(nb.a[2]);
	for(int i=1;i<=6;++i) get(c[i]);
	int ans=dfs(1,0);
	if(!ans) puts("Bob");
	else if(ans==1) puts("Draw");
	else puts("Alice");
}
int main()
{
	pm[0]=1;for(int i=1;i<=6;++i) pm[i]=pm[i-1]*3;
	to['S']=1,to['H']=2,to['C']=3,to['D']=4;
	to['A']=13,to['2']=1,to['3']=2,to['4']=3,to['5']=4,to['6']=5,to['7']=6,to['8']=7,to['9']=8,
	to['T']=9,to['J']=10,to['Q']=11,to['K']=12;
	int T=in();
	while(T--) solve();
	return 0;
}

F Longest Common Subsequence

题意:给定两个长度分别为 n , m n,m n,m 的序列 S , T S,T S,T,问其最长公共子序列长度。其中 S S S T T T 都是通过 x i + 1 = f ( x i ) = ( a x i 2 + b x i + c )   m o d   p x_{i+1}=f(x_i)=(ax_i^2+bx_i+c) \bmod p xi+1=f(xi)=(axi2+bxi+c)modp 迭代产生。 ∣ S ∣ , ∣ T ∣ ≤ 1 × 1 0 6 |S|,|T| \leq 1\times 10^6 S,T1×106

解法:容易注意到,若 S i = T j S_i=T_j Si=Tj,则从这两个位置开始,后面都一定完全一样,因而对答案的贡献为 min ⁡ ( n − i + 1 , m − j + 1 ) \min(n-i+1,m-j+1) min(ni+1,mj+1)。因而可以直接暴力使用 map记忆 S S S 中每个元素的第一次出现位置。这样的时间复杂度为 O ( n log ⁡ n ) \mathcal O(n \log n) O(nlogn)

基于以上性质,可以发现二者匹配的一定是一个后缀。因而对两个串进行翻转然后跑对两个串依次做一次字典串,利用 KMP 也可以得到答案。这样的复杂度为 O ( n ) \mathcal O(n) O(n)

#include <bits/stdc++.h>
using namespace std;
const int N = 1000000;
long long s[N + 5], t[N + 5];

int main()
{
    int caset, n, m;
    long long p, a, b, c, x;
    scanf("%d", &caset);
    while(caset--)
    {
        map<long long, int> pos;
        scanf("%d%d%lld%lld%lld%lld%lld", &n, &m, &p, &x, &a, &b, &c);
        for (int i = 1; i <= n;i++)
        {
            s[i] = x = (a * x % p * x % p + b * x % p + c) % p;
            if (pos.count(s[i]) == 0)
                pos[s[i]] = i;
        }
        int ans = 0;
        for (int i = 1; i <= m;i++)
        {
            t[i] = x = (a * x % p * x % p + b * x % p + c) % p;
            if (pos.count(x))
                ans = max(ans, min(m - i + 1, n - pos[x] + 1));
        }
        printf("%d\n", ans);
    }
    return 0;
}

G Lexicographic Comparison

题意:给定两个长度均为 n n n 的排列 A , P A,P A,P。有以下 q q q 次三类操作:

  1. 交换 A x A_x Ax A y A_y Ay
  2. 交换 P x P_x Px P y P_y Py
  3. 查询 A P x AP^x APx A P y AP^y APy 的大小。其中 A P i AP^i APi 表示排列 A A A P P P 置换操作下迭代 i i i 轮得到的置换。 x , y ≤ 1 × 1 0 18 x,y \leq 1\times 10^{18} x,y1×1018

n , q ≤ 1 × 1 0 5 n,q \leq 1\times 10^5 n,q1×105

解法:考虑维护 P P P 置换构成的若干个环。显然根据环大小,同时存在的只有 O ( n ) \mathcal O(\sqrt n) O(n ) 个环。而同一环大小在同一置换次数下一定都处于同一环状态,因而只需要保留环上出现位置最早的那一个环即可。真正在查询操作的时候,枚举每一个环大小,找到出现最早的不同的置换环(若环大小为 i i i,只要满足 x   m o d   i ≠ y   m o d   i x \bmod i \neq y \bmod i xmodi=ymodi 则必然是不同状态),单独考虑它即可。

因而问题转化为,如何快速的维护这些环,需要支持环的拆分和合并,以及快速查询环上第 k k k 个元素。可以考虑使用平衡树维护。将所有的环依照下标最小元素作为第一个元素展开成链,形成一个平衡树森林。

首先利用平衡树实现 moveFront 操作:将链 [ l 1 , l 2 , ⋯   , l x , ⋯   , l y , ⋯   , l k ] [l_1,l_2,\cdots,l_x,\cdots,l_y,\cdots,l_k] [l1,l2,,lx,,ly,,lk] 中的 [ x , y ] [x,y] [x,y] 平移到最前。这一步操作可以通过将 x x x splay 到根,将 x x x 现在的左儿子( [ l 1 , l 2 , ⋯   , l x − 1 ] [l_1,l_2,\cdots,l_{x-1}] [l1,l2,,lx1] 链部分)摘除,接到整条链的最后面,即 l k l_k lk 的右儿子处。

对于合并环操作,仅考虑 p x → x p_x \to x pxx p y → y p_y \to y pyy 两条边的影响,即是 [ x , ⋯   , p x ] [x,\cdots,p_x] [x,,px] [ y , ⋯   , p y ] [y,\cdots,p_y] [y,,py] 链转化为 [ x , ⋯   , p x , y , ⋯   , p y ] [x,\cdots,p_x,y,\cdots,p_y] [x,,px,y,,py],因而将 y y y 的子树接到 p x p_x px x x x 链上最后一个节点)。

对于拆分环操作,仅考虑同一环上的 p x → x p_x \to x pxx p y → y p_y \to y pyy,首先将 x x x 通过 moveFront 移动到链的最前端,此时链上关系为 [ x , ⋯   , p y , y , ⋯   , p x ] [x,\cdots,p_y,y,\cdots,p_x] [x,,py,y,,px],需要转化为 [ x , ⋯   , p y ] [x,\cdots,p_y] [x,,py] [ y , ⋯   , p x ] [y,\cdots,p_x] [y,,px]。将 p y p_y py 旋转到根,将 y y y 旋转到 p y p_y py 的右儿子,直接摘除 p y p_y py 的右子树即可。

因而查询复杂度 O ( n ) \mathcal O(\sqrt n) O(n ),修改操作 O ( log ⁡ n ) \mathcal O(\log n) O(logn)

#include <bits/stdc++.h>
using namespace std;
const int N = 100000;
int n, B, A[N + 5], P[N + 5], Qsiz[N + 5];
set<int> S1[320], S2;
class Splay
{
    struct node
    {
        int ch[2];
        int father;
        int minid;
        int siz;
        node()
        {
            ch[0] = ch[1] = 0;
            father = minid = siz = 0;
        }
    } NIL;
    vector<node> t;
    int tot;
    void new_node(int &place, int father)
    {
        place = ++tot;
        node temp = NIL;
        temp.father = father;
        temp.siz = 1;
        temp.minid = place;
        t.push_back(temp);
    }
	void update(int place)
    {
        t[place].minid = place;
        t[place].siz = 1;
        for (int i = 0; i <= 1;i++)
            if (t[place].ch[i])
            {
                t[place].minid = min(t[place].minid, t[t[place].ch[i]].minid);
                t[place].siz += t[t[place].ch[i]].siz;
            }
    }
    void rotate(int now)
    {
        int pre = t[now].father;
        int p = t[pre].father;
        int dir = (t[pre].ch[0] == now);
        t[now].father = p;
        t[pre].father = now;
        t[t[now].ch[dir]].father = pre;
        if (p)
            t[p].ch[t[p].ch[1] == pre] = now;
        t[pre].ch[dir ^ 1] = t[now].ch[dir];
        t[now].ch[dir] = pre;
        update(pre);
    }
    void splay(int place, int tar = 0)
    {
        while (t[place].father != tar)
        {
            int pre = t[place].father;
            if (t[pre].father != tar)
            {
                if ((t[t[pre].father].ch[0] == pre) ^ (t[pre].ch[0] == place))
                    rotate(place);
                else
                    rotate(pre);
            }
            rotate(place);
        }
        update(place);
    }
    int get_root(int x)
    {
        splay(x);
		return t[x].minid;
    }
    void add(int x)//只是在 set 中添加,不会在平衡树上真的添加
    {
        splay(x);
        Qsiz[t[x].minid] = t[x].siz;
        if (t[x].siz <= B)
            S1[t[x].siz].insert(t[x].minid);
        else
            S2.insert(t[x].minid);
    }
    void del(int x)//只是在 set 中删除,不会在平衡树上真的删除
    {
        splay(x);
        if (t[x].siz <= B)
            S1[t[x].siz].erase(t[x].minid);
		else
			S2.erase(t[x].minid);
    }
    int get(int place, int dir)//找到place链最前面和最后面元素
    {
        splay(place);
        int now = place;
        while (t[now].ch[dir])
            now = t[now].ch[dir];
        splay(now);
        return now;
    }
    int get_rank(int now, long long p)
    {
        splay(now);
        p = (p - 1) % t[now].siz + 1;
        p = (1 + t[now].siz - p) % t[now].siz + 1;
        while (1)
        {
            if (t[t[now].ch[0]].siz + 1 == p)
                break;
            else if (t[t[now].ch[0]].siz >= p)
                now = t[now].ch[0];
            else
            {
                p -= t[t[now].ch[0]].siz + 1;
                now = t[now].ch[1];
            }
        }
        splay(now);
        return now;
    }
    void moveFront(int x)
    {
        splay(x);
        int y = t[x].ch[0];
        if (!y)
            return;
        t[x].ch[0] = 0;
        update(x);
        int r = get(x, 1);
        t[r].ch[1] = y;
        update(r);
        t[y].father = r;
        splay(x);
    }
    void split(int x, int y)//分割环
    {
        del(x);
        moveFront(x);
        int z = P[y];//从P[y]进行切割,即将摘除[y,z]
        splay(z), splay(y, z);//将y的子树(y产生的新环)移动到z
        t[z].ch[1] = 0;//拆分
        update(z);
        t[y].father = 0;
        add(x), add(y);
    }
    void merge(int x, int y)//合并环
    {
        del(x), del(y);
        moveFront(x), moveFront(y);
        int z = get(x, 1);
        t[z].ch[1] = y;//将y接到x所在树上
        update(z);
        t[y].father = z;
        add(x);
    }

public:
    Splay(int n)
    {
        t.push_back(NIL);
        tot = 0;
        for (int i = 1; i <= n; i++)
        {
            new_node(i, 0);
            add(i);
        }
    }
    void update_cyc(int x, int y)
    {
        if (x == y)
            return;
        int p = get_root(x), q = get_root(y);
        if (p == q)
            split(x, y);
        else
            merge(x, y);
        swap(P[x], P[y]);
    }
    int query(long long x, long long y, int t)
    {
        moveFront(t);
        int px = get_rank(t, x), py = get_rank(t, y);
        if (A[px] > A[py])
            return 1;
        else if (A[px] < A[py])
            return -1;
        else
            return 0;
    }
};
char buf[10];
int main()
{
	int caset, q;
    long long x, y;
    scanf("%d",&caset);
	while (caset--)
    {
        scanf("%d%d", &n, &q);
        B = sqrt(n) + 1;
        for (int i = 1; i <= n; i++)
            A[i] = P[i] = i;
        Splay solve(n);
        auto query = [&](long long x, long long y)
        {
            if (x == y)
                return 0;
            int t = n + 1;
            for (int i = 1; i <= B; i++)
            {
                if (S1[i].empty() || x % i == y % i)
                    continue;
                t = min(t, *S1[i].begin());
            }
            for (int i : S2)
            {
                int s = Qsiz[i];
                if (x % s == y % s)
                    continue;
                t = min(t, i);
            }
            if (t > n)
                return 0;
            return solve.query(x, y, t);
        };
        while (q--)
        {
            scanf("%s%lld%lld", buf, &x, &y);
            if (buf[0] == 'c')
            {
                int t = query(x, y);
                if (t == -1)
                    printf("<\n");
                else if (t == 1)
                    printf(">\n");
				else
					printf("=\n");
            }
            else if (buf[5] == 'a')
                swap(A[x], A[y]);
            else
                solve.update_cyc(x, y);
        }
        for (int i = 1; i <= B; i++)
            S1[i].clear();
        S2.clear();
	}
	return 0;
}

I Equivalence in Connectivity

题意:给定 k k k n n n 的点的图。对于第 i i i 个图,其由 p i p_i pi 图删除或者新增一条边构成(保证 p i < i p_i<i pi<i),问这 k k k 张图依据连通性可以分成多少组。 n , k ≤ 1 × 1 0 5 n,k \leq 1\times 10^5 n,k1×105

解法:显然可以注意到,对图的操作序列可以构成一棵树。首先简化问题:若每次只在上一张图上进行删除或新增操作,如何实现。

显然维护连通性可以用并查集,但是并查集无法维护删除操作,因而考虑只能添加不能删除。那么使用线段树分治:将这个操作序列建立一颗线段树,维护每条边出现的时间,必定是线段树上一段或几段的连续序列。然后再去遍历线段树,将添加的边下放到叶子节点,即可完成每个叶子节点的状态更新(并查集更新)。对于树上遍历的回溯操作,即是进行并查集的撤销操作。因而需要维护一个可撤销的并查集。

注意:只需要一个并查集即可。因为线段树在遍历的每一个时刻只会面对一个操作局面,因而只需要在这个状态下不断的添加边或者回溯到历史版本即可。

接下来要处理的问题是如何将这么多的状态的并查集去归类合并。一个精巧的办法是,给图上每个点一个随机权值,每个连通块的权值为连通块内所有点的权值异或和,再将所有连通块的权值和加起来作为哈希值。那么这样合并两个连通块只需要将两个连通块的权值异或起来即可,便于合并操作。此题卡单哈希,因而需要将这个随机数取得更大或者使用更多次的哈希。

最后,本问题是建立在树上的(操作序列为一棵树),但是容易发现,对于一条边的增加和删除,只不过是变成了子树内全部都要增加与子树内全部都要删除。那么只需要对操作树进行一次 dfs 序列化,即可将问题转化到序列上,因而整个问题就圆满解决。整体时间复杂度 O ( n log ⁡ 2 n ) \mathcal O(n \log^2 n) O(nlog2n)

#include<bits/stdc++.h>
#define IL inline
#define LL long long
#define pb push_back
#define abs(x,y) (x<y?y-x:x-y)
using namespace std;
const LL N=1e5+3;
struct hh{
	LL to,nxt;
}e[N<<1];
struct kk{
	LL op,x,y;
	bool operator<(const kk &a) const{
	return x^a.x?x<a.x:y<a.y;}
}a[N],hsh[N];
struct zz{
	LL ld,lm,x,y;
};
LL n,m,k,num,fir[N],dfn[N],fa[N],siz[N],f1[N],f2[N],rev[N];
map<LL,LL>mp;
vector<LL>ans[N];
IL LL in(){
	char c;LL f=1;
	while((c=getchar())<'0'||c>'9')
	  if(c=='-') f=-1;
	LL x=c-'0';
	while((c=getchar())>='0'&&c<='9')
	  x=x*10+c-'0';
	return x*f;
}
IL LL mod(LL x){return x;}
IL LL find(LL x){
	while(x^fa[x]) x=fa[x];
	return x;
}
struct segment{
	#define ls k<<1
	#define rs k<<1|1
	#define pb push_back
	vector<kk>e[N<<2];vector<kk>fn[N<<2];
	void clear(LL k,LL l,LL r){
		e[k].clear(),fn[k].clear();
		if(l==r) return;
		LL mid=l+r>>1;
		clear(ls,l,mid),clear(rs,mid+1,r);
	}
	void ins(LL k,LL l,LL r,LL ll,LL rr,kk x){
		if(ll>rr) return;
		if(l>=ll&&r<=rr){e[k].pb(x);return;}
		LL mid=l+r>>1;
		if(ll<=mid) ins(ls,l,mid,ll,rr,x);
		if(rr>mid) ins(rs,mid+1,r,ll,rr,x);
	}
	IL void merge(LL x,LL y,LL k,LL &sum1,LL &sum2){
		LL fx=find(x),fy=find(y);
		if(fx^fy){
			if(siz[fx]<siz[fy]) swap(fx,fy);
			fn[k].pb((kk){f1[fx],fx,fy});
			sum1=mod(sum1-mod(f1[fx]+f1[fy])),
			sum2=mod(sum2-mod(f2[fx]+f2[fy])),
			fa[fy]=fx,siz[fx]+=siz[fy],f1[fx]^=f1[fy],f2[fx]^=f2[fy];
			sum1=mod(sum1+f1[fx]),sum2=mod(sum2+f2[fx]);
		}
	}
	void dfs(LL k,LL l,LL r,LL sum1,LL sum2){
		for(LL i=e[k].size()-1;~i;--i) merge(e[k][i].x,e[k][i].y,k,sum1,sum2);
		LL mid=l+r>>1;
		if(l==r) hsh[rev[l]]=(kk){0,sum1,sum2};
		else dfs(ls,l,mid,sum1,sum2),dfs(rs,mid+1,r,sum1,sum2);
		for(LL i=fn[k].size()-1;~i;--i){
			LL x=fn[k][i].x,y=fn[k][i].y;
			f2[x]^=f2[y],f1[x]^=f1[y],siz[x]-=siz[y],fa[y]=y;
		}
	}
}T;
IL void clear(){
	memset(fir+1,num=0,8*k);
	mp.clear(),T.clear(1,1,k);
	for(LL i=1;i<=k;++i) ans[i].clear();
}
IL void add(LL x,LL y){e[++num]=(hh){y,fir[x]},fir[x]=num;}
IL LL get(LL x,LL y){
	if(x<y) swap(x,y);
	return 1ll*(x-1)*n+y-1;
}
void dfs(LL u){
	rev[dfn[u]=++num]=u;LL cnt=get(a[u].x,a[u].y);
	if(u^1){
		if(a[u].op==1) mp[cnt]=num;
		else{
			T.ins(1,1,k,mp[cnt],num-1,a[u]);
			mp.erase(cnt);
		}
	}
	for(LL i=fir[u],v;v=e[i].to;i=e[i].nxt) dfs(v);
	if(u^1){
		if(a[u].op==1){
			T.ins(1,1,k,mp[cnt],num,a[u]);
			mp.erase(cnt);
		}
		else mp[cnt]=num+1;
	}
}
void solve(){
	char s[10];LL x,y,z,sum1=0,sum2=0;
	k=in(),n=in(),m=in();
	clear();
	for(LL i=1;i<=m;++i) x=in(),y=in(),mp[get(x,y)]=1;
	for(LL i=2;i<=k;++i){
		x=in(),scanf("%s",s+1),add(x,i);
		a[i]=(kk){(s[1]=='a')?1:-1,in(),in()};
	}
	num=0,dfs(1);
	for(map<LL,LL>:: iterator it=mp.begin();it!=mp.end();++it)
	  T.ins(1,1,k,it->second,k,(kk){0,it->first/n+1,it->first%n+1});
	for(LL i=1;i<=n;++i) fa[i]=i,siz[i]=1,f1[i]=rand(),f2[i]=rand(),sum1=mod(sum1+f1[i]),sum2=mod(sum2+f2[i]);
	T.dfs(1,1,k,sum1,sum2);LL cnt=0;map<kk,LL>mp;
	for(int i=1;i<=k;++i)
	  if(!mp.count(hsh[i])) mp[hsh[i]]=++cnt,ans[cnt].pb(i);
	  else ans[mp[hsh[i]]].pb(i);
	printf("%d\n",cnt);
	for(int i=1;i<=cnt;++i){
		printf("%d ",ans[i].size());
		for(int j=0;j<ans[i].size();++j)
	  	printf("%d ",ans[i][j]);
	  putchar('\n');
	}
}
int main()
{
	srand(time(0));
	int T=in();
	while(T--) solve();
	return 0;
}

K Symmetry: Convex

题意:给定 n n n 个点的凸多边形 C n C_n Cn,输出前 i i i 个点构成的凸包的对称轴。 n ≤ 1 × 1 0 5 n \leq 1\times 10^5 n1×105

解法:找对称轴可以使用 Manacher。本题先预处理整个凸包构成的串的 Manacher(注意不要倍长),然后依次加入点去考虑。

容易发现一个性质:随着 i i i 的增大, ∠ C i C 0 C 1 \angle C_iC_0C_1 CiC0C1 是在不断变大的。除非该对称轴是其角平分线,则该角必然与另一个角大小相同(对称)。使用一个 map记录每个角的大小,然后去枚举 ∠ C i C 0 C 1 \angle C_iC_0C_1 CiC0C1 角对称的角在哪里,使用 Manacher 预处理出来的回文半径再附带考虑几个新添加的角和边即可。代码中有更详细的解释。

#include <bits/stdc++.h>
using namespace std;

using _T = long long; // 全局数据类型,可修改为 long long 等

constexpr _T eps = 0;
constexpr long double PI = 3.1415926535897932384l;

// 点与向量
template<typename T> struct point
{
	T x, y;

	bool operator==(const point &a) const { return (abs(x - a.x) <= eps && abs(y - a.y) <= eps); }
	bool operator<(const point &a) const
	{
		if (abs(x - a.x) <= eps)
			return y < a.y - eps;
		return x < a.x - eps;
	}
	bool operator>(const point &a) const { return !(*this < a || *this == a); }
	point operator+(const point &a) const { return {x + a.x, y + a.y}; }
	point operator-(const point &a) const { return {x - a.x, y - a.y}; }
	point operator-() const { return {-x, -y}; }
	point operator*(const T k) const { return {k * x, k * y}; }
	point operator/(const T k) const { return {x / k, y / k}; }
	T operator*(const point &a) const { return x * a.x + y * a.y; } // 点积
	T operator^(const point &a) const { return x * a.y - y * a.x; } // 叉积,注意优先级
	int toleft(const point &a) const
	{
		const auto t = (*this) ^ a;
		return (t > eps) - (t < -eps);
	}									   // to-left 测试
	T len2() const { return (*this) * (*this); }				  // 向量长度的平方
	T dis2(const point &a) const { return (a - (*this)).len2(); } // 两点距离的平方

	// 涉及浮点数
	long double len() const { return sqrtl(len2()); }																	   // 向量长度
	long double dis(const point &a) const { return sqrtl(dis2(a)); }													   // 两点距离
	long double ang(const point &a) const { return acosl(max(-1.0, min(1.0, ((*this) * a) / (len() * a.len())))); }		   // 向量夹角
	point rot(const long double rad) const { return {x * cos(rad) - y * sin(rad), x * sin(rad) + y * cos(rad)}; }		   // 逆时针旋转(给定角度)
	point rot(const long double cosr, const long double sinr) const { return {x * cosr - y * sinr, x * sinr + y * cosr}; } // 逆时针旋转(给定角度的正弦与余弦)
};

using Point = point<_T>;

// 直线
template<typename T> struct line
{
	point<T> p, v; // p 为直线上一点,v 为方向向量

	bool operator==(const line &a) const { return v.toleft(a.v) == 0 && v.toleft(p - a.p) == 0; }
	int toleft(const point<T> &a) const { return v.toleft(a - p); } // to-left 测试

  // 涉及浮点数
	point<T> inter(const line &a) const { return p + v * ((a.v ^ (p - a.p)) / (v ^ a.v)); } // 直线交点
	long double dis(const point<T> &a) const { return abs(v ^ (a - p)) / v.len(); }			// 点到直线距离
	point<T> proj(const point<T> &a) const { return p + v * ((v * (a - p)) / (v * v)); }	// 点在直线上的投影
};

using Line = line<_T>;

vector<int> Manacher(vector<long long> &s)
{
    int n = s.size();
    vector<int> p(n, 1);
    for (int i = 0, r = 0, m = 0; i < n; i++)
    {
        if (i < r)
            p[i] = min(p[m * 2 - i], r - i);
        while (i >= p[i] && i + p[i] < n && s[i - p[i]] == s[i + p[i]])
            p[i]++;
        if (i + p[i] > r)
        {
            m = i;
            r = i + p[i];
        }
    }
    return p;
}

Line prep(Point a, Point b)//中垂线,经过的点为(A.x+B.x, A.y+B.y) 最后输出要还原
{
    Point dir = (Point){b.y - a.y, a.x - b.x}, p = a + b;
    return (Line){p, dir};
}
void print(Line l)
{
    long long a = -l.v.y, b = l.v.x;
    long long c = -a * l.p.x - b * l.p.y;
    a <<= 1;
    b <<= 1;
    long long d = __gcd(abs(c), __gcd(abs(a), abs(b)));
    printf("%lld %lld %lld\n", a / d, b / d, c / d);
}

int main()
{
    int t, n;
    scanf("%d", &t);
    while(t--)
    {
        scanf("%d", &n);
        vector<Point> C(n);
        for (int i = 0; i < n;i++)
            scanf("%lld%lld", &C[i].x, &C[i].y);
        vector<long long> s;
        for (int i = 0; i < n;i++)
        {
            s.push_back(C[i].dis2(C[(i + 1) % n]));
            s.push_back((C[i] - C[(i + 1) % n]) * (C[(i + 2) % n] - C[(i + 1) % n]));
        }
        map<long long, vector<int>> ang;
        for (int i = 1; i < 2 * n; i += 2)
            ang[s[i]].push_back(i);
        auto para = Manacher(s);
        for (int i = 2; i < n;i++)
        {
            vector<Line> ans;
            if (para[i] >= i && s[0] == C[i].dis2(C[0]))// i01角平分线,用i的对踵点/边(para数组中的第i位)判断。若刚好能覆盖到0,则除了01和0i的边其余均相等。
                ans.push_back(prep(C[1], C[i]));
            auto lastang = (C[1] - C[0]) * (C[i] - C[0]);
            if (para[i - 1] >= i && lastang == (C[0] - C[i]) * (C[i - 1] - C[i]))//0i的中垂线。用0i的对踵点(para中第i-1位判断)是否能覆盖到0,剩下要判断的就是i01角和0i边
                ans.push_back(prep(C[0], C[i]));
            //接下来将整个凸包分成[0,j] 侧和 [j,i] 侧,0与j角对称相等
            for (auto j : ang[lastang])
            {
                if (j >= 2 * i - 1)
                    break;
                if (para[(j - 1) / 2] < (j - 1) / 2 + 1) //确保[0,j]侧匹配(对称),即是找这一段的中点,查询其覆盖半径能否到达0
                    continue;
                if (j == 2 * i - 3) //(i-1)i0的角平分线
                {
                    if (C[i].dis2(C[0]) == C[i].dis2(C[i - 1]))
                        ans.push_back(prep(C[0], C[i - 1]));
                }
                else
                {
                    int outercen = (2 * i - 2 + j + 3) / 2;
                    if (para[outercen] >= (2 * i - 2 - j - 3) / 2 + 1)//外侧[j,i]可以匹配
                    {
                        int oriid = (j + 1) / 2;
                        if (C[i].dis2(C[0]) == C[oriid].dis2(C[oriid + 1]) && (C[0] - C[i]) * (C[i - 1] - C[i]) == (C[oriid] - C[oriid + 1]) * (C[oriid + 2] - C[oriid + 1]))
                        //最后的一条边和一个角判断。这一段由于是新加的因而需要手动判断
                            ans.push_back(prep(C[0], C[oriid]));
                    }
                }
            }
            printf("%d\n", ans.size());
            for (auto j : ans)
                print(j);
        }
    }
    return 0;
}
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值