2024.4.20 筆記

PassName發表於2024-04-20

2024.4.20 筆記

SP4354 Snowflakes

記錄所有的雪花,判斷是否存在兩個雪花是相同的。由於資料量較大,需要 \(O(n)\) 的複雜度來查詢雪花,考慮雜湊表

定義一個雜湊值的轉換方式,讓不同的雪花雜湊值不相同,相同的雪花的六個角一定是相同的 \(6\) 個值且相同的順序排列,只不過起點在不同的角上。因此可以將雜湊值定義為每朵雪花的六個角的長度之和 \(+\) 六個角的長度乘積。

然後還需要判斷兩個雪花是否相同,不能使用雜湊值比較的方法,因為可能會產生雜湊衝突,因此可以使用雪花的特性,兩個相同的雪花,各自從某一角開始順時針或逆時針記錄長度,能得到兩個相同的六元組。我們可以基於這個特性直接暴力判斷。

int n;
int snow[N][6];
int h[N], ne[N], idx;
int t[6];

int get_hash(int a[]) 
{
    int res1 = 0, res2 = 0;
    for (rint i = 0; i < 6; i++)
    {
		res1 = (res1 + a[i]) % P;
		res2 = (res2 * a[i]) % P;
	}
    return (res1 + res2) % P;
}

bool check(int a[], int b[])
{
    for (rint i = 0; i < 6; i++)
    {
        for (rint j = 0; j < 6; j++)
        {
            bool flag = 1;
            for (rint k = 0; k < 6; k++) 
                if (a[(i + k) % 6] != b[(j + k) % 6])
                    flag = 0;
            if (flag) return 1;
            flag = 1;
            for (rint k = 0; k < 6; k++)
                if (a[(i + k) % 6] != b[(j - k) % 6]) 
				    flag = 0;
            if (flag) return 1;
        }		
	}
    return 0;
}

bool insert(int a[]) 
{
    int x = get_hash(a);
    for (rint i = h[x]; i; i = ne[i]) 
    {
        if (check(snow[i], a)) return 1;
	}
    idx++;
    for (rint i = 0; i < 6; i++) snow[idx][i] = a[i];		
    ne[idx] = h[x];
    h[x] = idx; 
    return 0;
}

signed main()
{
    cin >> n;
    while (n--)
    {
        for (rint i = 0; i < 6; i++)
        {
			cin >> t[i];
		}
        if (insert(t))
        {
            puts("Twin snowflakes found.");
            return 0;
        }
    }
    puts("No two snowflakes are alike.");
    
    return 0;
}

AcWing 138. 兔子與兔子

本題每次要比較的是字串中的某兩個區間是否相同,可以用字串雜湊來做,只需要使該區間內雜湊值一樣即可

int n, m;
char s[N];
//h[i] 表示原字串中前 i 個字元組成的字串的雜湊值
//p[i] 表示 p 的 i 次方
uint h[N], p[N];

uint calc(int l, int r) 
{
    return h[r] - h[l - 1] * p[r - l + 1];
}

signed main()
{
    scanf("%s", s + 1);
    n = strlen(s + 1);

    p[0] = 1; 
    for (rint i = 1; i <= n; i++) 
    {
        p[i] = p[i - 1] * P;
        h[i] = h[i - 1] * P + s[i];
    }

    cin >> m;
    while (m--)
    {
        int l1, r1, l2, r2;
        cin >> l1 >> r1 >> l2 >> r2;
        if (calc(l1, r1) == calc(l2, r2)) puts("Yes"); 
        else puts("No"); 
    }
    return 0;
}

AcWing 139. 迴文子串的最大長度

由於 zty 講的是雜湊,就不用manacher了

本題要求的是一個字串中最大回文串的長度,我們可以列舉中間點,然後每次求出當前中間點的最大回文串,對所有情況取一個最大值即可。

但是對於中間點有兩種情況,如果字串是奇數個,那麼是存在中間點的,但是如果字串是偶數個,那麼是不存在中間點的。這裡我們可以用一個常用技巧來簡化判斷,將字串中每兩個字元之間加上一個特殊字元,假設加上一個 '#'
對於奇數個的字串,a#b#c#d#f,新增後還是奇數個。對於偶數個的字串,a#b#c#d,新增後變成了奇數個。透過這樣的處理,我們只需要考慮奇數情況的字串就行了,奇數個的字串一定是存在中間點的,因此直接列舉中間點即可。

然後就要對於每個中間點求最大回文串的長度,可以求當前中間點兩邊需要加上的邊長,然後二分求這個邊長的最大值。每次二分出最大值後統計一下回文串的長度,更新最大值即可。

int n;
char s[N];
//h[] 表示正序的字串雜湊值
//rh[] 表示倒序的字串雜湊值
//p[i] 表示p的i次方
uint h[N], rh[N], p[N];

uint calc(uint h[], int l, int r) 
{
    return h[r] - h[l - 1] * p[r - l + 1];
}

signed main()
{
    int T = 1;
    while (scanf("%s", s + 1), strcmp(s + 1, "END"))
    {
        n = strlen(s + 1);
        for (rint i = n * 2; i >= 1; i -= 2) 
		//在字串的每兩個字元之間插入一個相同的數
        {
            s[i] = s[i / 2];
            s[i - 1] = 'z' + 1;
        }
        n *= 2; //更新字串的長度
        p[0] = 1; 
        for (rint i = 1, j = n; i <= n; i++, j--) 
        {
            p[i] = p[i - 1] * P;
            h[i] = h[i - 1] * P + s[i]; 
            rh[i] = rh[i - 1] * P + s[j]; 
        }

        int res = 0; 
		//記錄最大回文串的長度
        for (rint i = 1; i <= n; i++)
		//列舉中間值
        {
            int l = 0, r = min(i - 1, n - i);
            while (l < r)
            {
                int mid = (l + r + 1) >> 1;
                //如果兩邊的字串相等說明當前邊長已經是迴文串,那麼可以繼續擴大邊長
                if (calc(h, i - mid, i - 1) == calc(rh, n - (i + mid) + 1, n - (i + 1) + 1)) l = mid;
                else r = mid - 1; 
				//否則說明不是迴文串,那麼更大的邊長也不能組成迴文串,因此需要縮小邊長
            }

            if(s[i - l] <= 'z') res = max(res, l + 1); 
			//如果頭和尾是字串中的字元,那麼整個迴文串的長度是邊長+1
            else res = max(res, l); 
			//如果頭和尾是額外新增的特殊字元,那麼整個迴文串的長度就是邊長
        }
        printf("Case %lld: %lld\n", T++, res);
    }
    return 0;
}

P3435 OKR-Periods of Words

本題是一個字串關於迴圈元的證明。

這裡直接得出結論:對於字串中每一位 is[i - ne[i] + 1 ~ i]s[1 ~ ne[i]] 都是相等的,並且不存在更大的 ne 值滿足這個條件

還能得出推論:最小迴圈節是 1-ne[i],次小迴圈節是 1-ne[ne[i]] ,依次能得出一個字串所有的迴圈節。

void get_next(char p[], int n)
{
    for (rint i = 2, j = 0; i <= n; i++)
    {
        while (j > 0 && p[i] != p[j + 1]) j = ne[j];
        if (p[i] == p[j + 1]) j++;
        ne[i] = j;
    }
}

signed main()
{
    scanf("%lld%s", &n, s + 1);
    get_next(s, n);
    for (rint i = 2, j = 2; i <= n; i++, j = i)
    {
        while (ne[j]) j = ne[j];
        if (ne[i]) ne[i] = j;//記憶化一下,不然會 TLE 30pts
        ans += i - j;
    }
    cout << ans << endl;
    return 0;
}

P5410 【模板】擴充套件 KMP

學的 George1123 佬的

這裡只給出程式碼

int ans1, ans2;
int z[M];
char a[N], b[N];
char new_s[M];

void exKMP_getZ(char s[])
{
	int n = strlen(s);	
	for (rint i = 1, j = 0; i < n; i++)
	{
		int k = i - j;
		if (z[j] - k > 0) z[i] = min(z[k], z[j] - k);
		while (z[i] + i < n && s[z[i]] == s[z[i] + i]) z[i]++;
		if (z[j] - z[i] < k) j = i;
	}
}

signed main()
{
	scanf("%s%s", a, b);
	int la = strlen(a);	
	int lb = strlen(b);
	for (rint i = 0; i < lb; i++) new_s[i] = b[i];	
	for (rint i = lb, j = 0; i < la + lb; i++, j++) new_s[i] = a[j];
	
	exKMP_getZ(new_s);
	
	for (rint i = 0; i < lb; i++)
	{
		if (!i) ans1 ^= lb + 1;
		else ans1 ^= (min(z[i], lb - i) + 1) * (i + 1);			
	}
	for (rint i = 0; i < la; i++) ans2 ^= (min(z[i + lb], lb) + 1) * (i + 1);
	cout << ans1 << endl << ans2 << endl;
	
	return 0;
}

AcWing 142. 字首統計

本題要求的是已知若干個字串,然後查詢出有多少個字串是給定查詢的字串的字首。

關於字首的統計可以用 Trie 樹來做,將已知的字串全部加入 Trie 樹中,在每個字串的結尾節點做上標記。

然後在 Trie 樹上查詢給定的字串,在查詢這個字串的路上到達的所有字首都是字串的字首,每走到一個節點就將標記上累計的字串個數累加到結果上。

int n, m;
int tr[N][27], tot = 1; 
int cnt[N];
char s[N];

void insert(char s[]) 
{ 
	int len = strlen(s), p = 1;
	for (rint k = 0; k < len; k++) 
	{
		int ch = s[k] - 'a';
		if (!tr[p][ch]) tr[p][ch] = ++tot;
		p = tr[p][ch];
	}
	cnt[p]++;
}

int search(char s[]) 
{
	int len = strlen(s), p = 1;
	int ans = 0;
	for (rint k = 0; k < len; k++) 
	{
		p = tr[p][s[k] - 'a'];
		if (!p) return ans;
		ans += cnt[p];
	}
	return ans;
}

signed main() 
{
	cin >> n >> m;
	for (rint i = 1; i <= n; i++) 
	{
		scanf("%s", s);
		insert(s);
	}
	for (rint i = 1; i <= m; i++) 
	{
		scanf("%s", s);
		cout << search(s) << endl;
	}
	return 0;
}

AcWing 143. 最大異或對

字典樹不單單可以高效儲存和查詢字串集合,還可以儲存二進位制數字

將每個數以二進位制方式存入字典樹,找的時候從最高位去找有無該位的異

void insert(int val) 
{ 
	int p = 1;
	for (rint k = 30; k >= 0; k--) 
	{
		int ch = val >> k & 1;
		if (!tr[p][ch]) tr[p][ch] = ++tot;
		p = tr[p][ch];
	}
}

int search(int val) 
{
	int p = 1;
	int ans = 0;
	for (rint k = 30; k >= 0; k--) 
	{
		int ch = val >> k & 1;
		if (tr[p][ch ^ 1]) 
		{ // 走相反的位
			p = tr[p][ch ^ 1];
			ans |= 1 << k;
		} 
		else 
		{ // 只能走相同的位
			p = tr[p][ch];
		}
	}
	return ans;
}

signed main() 
{
	cin >> n;
	for (rint i = 1; i <= n; i++) 
	{
		cin >> a[i];
		insert(a[i]);
		ans = max(ans, search(a[i]));
	}
	cout << ans << endl;
	return 0;
}

AcWing 144. 最長異或值路徑

首先可以用深搜求出所有點到根節點的異或距離,由於在二進位制中異或運算相當於減法,

因此對於 x->y 之間的異或路徑長度即可求解

我們現在需要列舉所有點,對於每個點x都求出和它的異或路徑的異或值最大的一個點 \(y\),那麼從 \(x\) 能走到的最長的異或路徑也可求解

要從 \(n\) 個數中選出兩個數,使得這兩個數的異或值最大,可以使用 Trie 樹快速求解

void add(int a, int b, int c)
{
	e[++idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx;
}

void dfs(int x, int father, int sum)
{
    a[x] = sum;
    for (rint i = h[x]; i; i = ne[i])
    {
        int y = e[i];
        if (y == father) continue; 
		dfs(y, x, sum ^ w[i]);
    }
}

void insert(int val) 
{ 
	int p = 1;
	for (rint k = 30; k >= 0; k--) 
	{
		int ch = val >> k & 1;
		if (!tr[p][ch]) tr[p][ch] = ++tot;
		p = tr[p][ch];
	}
}

int search(int val) 
{
	int p = 1;
	int ans = 0;
	for (rint k = 30; k >= 0; k--) 
	{
		int ch = val >> k & 1;
		if (tr[p][ch ^ 1]) 
		{ 
			p = tr[p][ch ^ 1];
			ans |= 1 << k;
		} 
		else 
		{ 
			p = tr[p][ch];
		}
	}
	return ans;
}

signed main()
{
    cin >> n;
    for (rint i = 1; i < n; i++)
    {
        int a, b, c;
        cin >> a >> b >> c;
        add(a, b, c);
        add(b, a, c);
    }
    dfs(0, 0, 0);
    for (rint i = 1; i <= n; i++) insert(a[i]);
    int res = 0;
    for (rint i = 1; i <= n; i++) res = max(res, search(a[i]));
    cout << res << endl;
    return 0;
}

AcWing 147. 資料備份

可以發現最優解中每兩個配對的辦公樓一定時相鄰的,因此我們可以計算一下每兩個相鄰的辦公樓之間的距離。

d[i] 表示第 \(i\) 個辦公樓和第 \(i+1\) 個辦公樓之間的距離。

那麼問題就變成了從 d[] 數列中選 \(k\) 個數,使它們的和最小,並且相鄰的兩個數不能被同時選(任一辦公樓都屬於唯一的配對組)

如果 k = 1,答案就是 d[] 數列中的最小值。
如果 k = 2,答案則一定是以下兩種情況:

    1. 選擇最小值 d[i],以及除了 d[i - 1], d[i], d[i + 1] 之外的其他數中的最小值
    1. 選擇最小值兩側的兩個數,d[i - 1]d[i + 1]

很容易證明,如果不選 d[i - 1]d[i + 1],那麼最優解一定選了 d[i],選了 d[i] 後不能選 d[i - 1]d[i + 1],因此還選了這三個數以外的最小值。如果選了d[i - 1]或d[i + 1]其中一個,由於d[i]的最小值,那麼這時將 d[i - 1]d[i + 1] 換成 d[i] 答案會更小,因此在最優解只有以上兩種情況,且 d[i] 兩則的數要麼都選要麼都不選。

因此,我們可以先選上最小值 d[i],然後把 d[i - 1], d[i], d[i + 1] 從數列中刪去,再在原位置加入 d[i - 1] + d[i + 1] - d[i],這時就變成了"從新的數列中選出不超過 \(k-1\) 個數,使它們的和最小,且相鄰兩個數不能同時選"這個子問題。

對於子問題,如果選了 d[i - 1] + d[i + 1] + d[i],相當於去掉 d[i],換上 d[i - 1]d[i + 1]。如果沒選,那麼剛才選出的 d[i] 加上這次選出的最小值就是最優解。這樣恰好涵蓋了最優解的兩種情況。

綜上所述,得出了本題的演算法:

建立一個連結串列,連線 \(n-1\) 個節點,分別表示 d[1], d[2], ..., d[n - 1],即每兩個辦公樓之間的距離。再建立一個最小堆,
與連結串列構成對映關係(即堆中也有 \(n-1\) 個節點,權值分別是 d[1], d[2], ..., d[n - 1],同時記錄對應的在連結串列中的下標)。

每次取出堆頂,把權值累加到答案中,設堆頂對應連結串列節點的下標為 p,數值為 w[p],在連結串列中刪除 p, p->prev, p->next
在同樣的位置插入一個新節點 q,記錄數值 w[q] = w[p->prev] + w[p->next] - w[p]。在堆中同時刪除對應的 p->prevp->next 的節點,
插入對應連結串列節點 q,權值為 w[q] 的新節點。

重複上述操作 \(K\) 次,就得到了最終答案。

int n, k;
int d[N];
int l[N], r[N]; 
//連結串列
int idx;
bool st[N]; 
//記錄某個節點是否被刪去
priority_queue<pii, vector<pii>, greater<pii> > h; 

void remove(int x) 
{ //刪除連結串列中某個元素
	st[x] = 1; 
	//記錄當前節點在堆中也被刪除
	r[l[x]] = r[x];
	l[r[x]] = l[x];
}

signed main() 
{
	cin >> n >> k;
	for (rint i = 0; i < n; i++) cin >> d[i];
	for (rint i = n - 1; i > 0; i--) d[i] -= d[i - 1];
	d[0] = d[n] = inf; //設定兩個邊界哨兵
	for (rint i = 1; i < n; i++) 
	{
		l[i] = i - 1;
		r[i] = i + 1;
		h.push({d[i], i}); 
		//加入堆中
	}
	int res = 0; //記錄最小總和
	for (rint i = 0; i < k; i++) 
	{
		while (st[h.top().second]) h.pop(); 
		//將所有應該刪去的節點刪去
		pii t = h.top(); 
		//取出堆頂
		h.pop();
		int v = t.first;
		int p = t.second, left = l[p], right = r[p];
		remove(left), remove(right); //刪去兩邊的節點
		res += v; //累加值
		d[p] = d[left] + d[right] - d[p]; //修改值
		h.push({d[p], p}); //將修改後的節點放回堆中
	}
	cout << res << endl;
	return 0;
}

AcWing 241. 樓蘭圖騰

從左向右依次遍歷每個數 \(a[i]\),使用樹狀陣列統計在 \(i\) 位置之前所有比 \(a[i]\) 大的數的個數、以及比 \(a[i]\) 小的數的個數。統計完成後,將 \(a[i]\) 加入到樹狀陣列。

從右向左依次遍歷每個數 \(a[i]\),使用樹狀陣列統計在 \(i\) 位置之後所有比 \(a[i]\) 大的數的個數、以及比 \(a[i]\) 小的數的個數。統計完成後,將 \(a[i]\) 加入到樹狀陣列。

int n, a[N];
int l[N], r[N];
int c[N];
int A, V; 

int lowbit(int x) {return x & -x;}

void add(int x, int k) 
{ 
	for (rint i = x; i <= n; i += lowbit(i)) c[i] += k;
}

int ask(int x) 
{ 
	int ans = 0;
	for (rint i = x; i; i -= lowbit(i)) ans += c[i];
	return ans;
}

signed main() 
{
	cin >> n;
	for (rint i = 1; i <= n; i++) cin >> a[i];
	for (rint i = 1; i <= n; i++) 
	{ 
	//因為從左往右遍歷並插值,所以在呼叫 ask 函式時 c 中存的都是第 i 個節點左邊的值
		int y = a[i]; //當前節點的高度
		l[i] = ask(y - 1); //找到當前節點左邊的比高度比 y 小的數的個數
		r[i] = ask(n) - ask(y);//找到當前節點左邊的比高度比 y 大的數的個數
		add(y, 1);//把 y 插入到 c 陣列中, 相當於建樹
	}

	memset(c, 0, sizeof c);
	//準備從右往左讀,再建一遍樹

	for (rint i = n; i >= 1; i--) 
	{ 
		int y = a[i];
		l[i] *= ask(y - 1);
		A += l[i];
		//以 y 為最高點的總方案數為 (y 左邊比 y 低的點數) * (y 右邊比 y 低的點數)
		r[i] *= ask(n) - ask(y);
		V += r[i];
		//以 y 為最低點的總方案數為 (y 左邊比 y 高的點數) * (y 右邊比 y 高的點數)
		add(y, 1);
	}

	cout << V << " " << A << endl;
	return 0;
}

P3605 Promotion Counting

求某節點子樹內比該節點的點權大的點的個數

int n, p[N], b[N], ans[N];
vector<int> e[N];

int c[N];
int lowbit(int x){ return x & -x;}
void add(int x, int y) 
{
	for (; x <= n; x += lowbit(x)) c[x] += y;
}
int query(int x)
{
	int ans = 0;
    for (; x; x -= lowbit(x)) ans += c[x];
    return ans;
}
	
void dfs(int x) 
{ 
	ans[x] = query(p[x]) - query(n); 
	for (auto y : e[x]) dfs(y); 
	ans[x] += (query(n) - query(p[x]));
	add(p[x], 1); 
}

signed main() 
{
	cin >> n;
	for (rint i = 1; i <= n; i++)
	{
		cin >> p[i];
		b[i] = p[i];
	}
		
	sort(b + 1, b + n + 1); 
	for (rint i = 1; i <= n; i++)
	{
		p[i] = lower_bound(b + 1, b + n + 1, p[i]) - b;		
	}

	for (rint i = 2; i <= n; i++) 
	{
		int x;
		cin >> x;
		e[x].push_back(i);
	}
	dfs(1);
	for (rint i = 1; i <= n; i++) cout << ans[i] << endl;
		
	return 0;
}

P4054 [JSOI2009] 計數問題

定義第一個維度為 \(x\),第二個維度為 \(y\),第三個維度為權值 \(c\)

定義兩個函式 \(add(x,y,c,d)\)\(sum(x,y,c)\)

  • \(add(x,y,c,d)\):將左上角點座標為 \((1,1)\),右下角點座標為 \((x,y)\) 的矩形中中權值 \(c\) 的格子的個數增加 \(d\)
  • \(sum(x,y,c)\) 統計左上角點座標為 \((1,1)\),右下角點座標為 \((x,y)\) 的矩形中權值為 \(c\) 的格子的個數。

因此當進行操作 1 時,將原先的權值出現次數 \(-1\),將修改後的權值的出現次數 \(+1\)

當進行操作 2 時,根據容斥原理,易得答案為 \(sum(x2, y2, c) - sum(x2, y1 - 1, c) - sum(x1 - 1, y2, c) + sum(x1 - 1, y1 - 1, c)\)

int lowbit(int x) {return x & (-x);}

void add(int x, int y, int k, int color) 
{
	for (rint i = x; i <= n; i += lowbit(i))
		for (rint j = y; j <= m; j += lowbit(j))
			c[i][j][color] += k;
}

int query(int x, int y, int color)
{
	int ans = 0;
	for (rint i = x; i; i -= lowbit(i))
		for (rint j = y; j; j -= lowbit(j))
			ans += c[i][j][color];
	return ans;
}

signed main() 
{
	cin >> n >> m;
	for (rint i = 1; i <= n; i++)
	{
		for (rint j = 1; j <= m; j++) 
		{
			cin >> color;
			a[i][j] = color;
			add(i, j, 1, color);
		}		
	}
	
	int T;
	cin >> T;
	while (T--)
	{
		int op;
		cin >> op;
		int x1, y1, x2, y2;
		if (op == 1) 
		{
			cin >> x1 >> y1 >> color;
			add(x1, y1, -1, a[x1][y1]);
			a[x1][y1] = color;
			add(x1, y1, 1, color);
		} 
		else 
		{
			cin >> x1 >> x2 >> y1 >> y2 >> color;
			cout << query(x2, y2, color) - query(x1 - 1, y2, color) - query(x2, y1 - 1, color) + query(x1 - 1, y1 - 1, color) << endl;
		}
	}
	return 0;
}