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,m≤30, T T T 测, T ≤ 1 × 1 0 5 T \leq 1\times 10^5 T≤1×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) ⌊2ni⌋⊕j⊕2m(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+m−x+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=⌊2nx⌋⊕2m(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=0∑X(⌊2nx⌋⊕2m(xmod2n))⊕i。对于此类 ∑ i i ⊕ x \sum_{i}i \oplus x ∑ii⊕x,其中 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=0∑X(⌊2ni⌋⊕2m(imod2n))⊕X。不难发现, ⌊ i 2 n ⌋ ∈ [ 0 , 2 m − 1 ] \left \lfloor \dfrac{i}{2^n}\right \rfloor \in [0,2^m-1] ⌊2ni⌋∈[0,2m−1],而 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 j≥m 时计算的时 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∣,∣T∣≤1×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(n−i+1,m−j+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 次三类操作:
- 交换 A x A_x Ax 与 A y A_y Ay;
- 交换 P x P_x Px 与 P y P_y Py;
- 查询 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,y≤1×1018。
n , q ≤ 1 × 1 0 5 n,q \leq 1\times 10^5 n,q≤1×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,⋯,lx−1] 链部分)摘除,接到整条链的最后面,即 l k l_k lk 的右儿子处。
对于合并环操作,仅考虑 p x → x p_x \to x px→x 与 p y → y p_y \to y py→y 两条边的影响,即是 [ 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 px→x 与 p y → y p_y \to y py→y,首先将 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,k≤1×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 n≤1×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;
}