2020.11.20 選拔賽題解

訓練賽地址c++

A (思惟)

題目連接
⭐⭐算法

題意:
給出下述兩種操做數組

\[\begin{cases} 0&x=x>>1\\ 1&x=1-x \end{cases} \]

如今給出\(a\)\(b\),問使得1經過上述操做變成\(\frac{a}{2^b}\)的最小長度指令序列(假設整數位的右邊就是對應的小數位)
保證\(a\)是奇數函數

解析:佈局

  1. 觀察原公式,無非是把\(a\)化成2進制,再將他向右邊移動\(n\)位。因爲數據條件的限制,這個數必定是一個小數。這樣就把問題轉化成如何在小數部分湊出\(a\)對應的二進制序列了。
    觀察操做指令,若是想要得到一個連續爲1的序列學習

    例如得到0.111,能夠先將1右移3位變成0.001,再經過1-x,變成所須要的0.111spa

  2. 能夠發現這個過程當中最後一位是保持不變的1,而以前產生的連續0由於1-x產生了反轉變成了連續1。在這樣的基礎上若是再次左移,就能夠得到連續的0,如此交替往復,咱們彷佛能夠獲得任何想要的二進制序列,也就達到了要求.net

  3. 對於最後一個出現的0(1),也就是第一位來講,它必定是1,因此當將這一位移動到小數部分時(也就是循環結束時),必需要進行1操做code

  4. 最後輸出剩餘的0便可排序

補充:當\(a\)爲奇數時,最後一位始終是1,而無論哪一種指令操做,所構成的小數最後一位也是1,兩者是相對應的,所以第一步永遠是0

特殊狀況:若是隻移動1位時,也就是0.1的狀況,這種時候先後位置不同,可是\(1-x=x\),不必進行多餘的1操做

總結:每次判斷轉化成的二進制數據,每讀取一位就進行0操做(右移),若是當前位與上一位不相等1操做(反轉,結束0或1開始錄入異位連續01序列),對移動1次進行特判,最後輸出剩餘0

#include<cstdio>
using namespace std;

long long a, b;

int main(void)
{
	scanf("%lld%lld", &a, &b);
	int c = 1;
	a >>= 1;
	//記錄上一次
	bool last = true;
	printf("0");
	while (a)
	{
		if ((a & 1) != last)
		{
			//特判
			if (c != 1)
				printf("1");
		}
		last = (a & 1);
		++c;
		printf("0");
		a >>= 1;
	}
	//特判
	if (c != 1)
		printf("1");
	for (int i = c; i < b; ++i)
		printf("0");
}

B (水題)

題目連接

題意:
給出五根木棍,問能組成多少個不一樣的三角形

解析:
枚舉三根木棍的組合狀況,兩邊之和大於第三邊斷定便可,再將這三個木棍從小到大排列用\(map\)記錄便可

錯誤之處:
\(dfs\)進行枚舉時,對存儲數據的數組直接進行排序,從而影響了搜索時的存儲順序
一直WA呀,QAQ

#include<cstdio>
#include<algorithm>
#include<map>
using namespace std;

int ret[3], ans, a[5];
map<int, map<int, map<int, bool> > > m;


void dfs(int cnt, int x)
{

	if (x == 5)
	{
		ans += cnt == 3 && ret[0] + ret[1] > ret[2] && ret[0] + ret[2] > ret[1] && ret[1] + ret[2] > ret[0] && !m[ret[0]][ret[1]][ret[2]];
		return;
	}
	dfs(cnt, x + 1);
	if (cnt < 3)
	{
		ret[cnt] = a[x];
		dfs(cnt + 1, x + 1);
	}
}

int main(void)
{
	for (int i = 0; i < 5; ++i)
		scanf("%d", &a[i]);
	//提早sort 保證枚舉出來的數列必定是非嚴格單調遞增的
	sort(a, a + 5);
	dfs(0, 0);
	printf("%d", ans);
}

C(組合數學)

題目連接
⭐⭐⭐
題意:
由‘A’,‘B’,‘C’三種類型字母組成的長度爲n的字符串,且對於連續的 k 個字符,A的數量 + B的數量等於C的數量,求方案數

解析:

  1. 由公式能夠看出A和B是等價的,因此能夠將A和B當作同一種,記做D。
  2. 對於連續\(k\)個字符要知足條件,能夠看做\(k\)在區間內滑動,每次丟棄左端點的字符,必定得在右端點添加相同類型的字符,這樣的狀況下,若是肯定了\(k\)中字符的字符排列,那麼總體的類型佈局也就肯定了
  3. 問題轉化成對於每種\(k\)區間內佈局,整個\(n\)區間中有多少個D

\[\underbrace{\overbrace{\underbrace{D(C)\cdots D(C)}_k\ \underbrace{D(C)\ \cdots\cdots \ D(C)}_{\left(\left\lfloor\frac{n}{k}\right\rfloor-1\right)\times k} }^{Part_1}\qquad \overbrace{\underbrace{D(C)\cdots D(C) }_{n\%k}}^{Part_2}}_{n}\]

 經過所給公式可知,在\(k\)長度的區間中\(|C|=|D|=\frac{k}{2}\),那麼對於每種排列布局,他們的\(1\)部分D的數量都是相同的,有\(\left\lfloor\frac{n}{k}\right\rfloor\times\frac{k}{2}\)個,即對應\(2^{\left\lfloor\frac{n}{k}\right\rfloor\times\frac{k}{2}}\)
 4. 對於\(2\)部分,能夠再次將區間\(k\)分爲長度爲\(n\% k\)的前半部分和剩餘的後半部分,對於前半部分,能夠挑選出\(i\)個位置給D存放,對應種類數爲\(C_{n\%k}^i\),同時後半部分要將剩下的\(\frac{k}{2}-i\)個D挑選完畢,即\(C_{k-n\%k}^{\frac{k}{2}-i}\),同時要注意越界溢出狀況,即

\[\{i\mid i\le n\%k\wedge\left(\frac{k}{2}-i\le k-n\%k\right)\wedge i\le \frac{k}{2}\} \]

再根據加法原理,能夠獲得後半部分對應的公式

\[\sum_{i=max\left(0,n\%k-\frac{k}{2}\right)}^{\min\left(n\%k,\frac{k}{2}\right)}C_{n\%k}^i\times C_{k-n\%k}^{\frac{k}{2}-i}\times 2^i \]

總結:

\[sum=2^{\left\lfloor\frac{n}{k}\right\rfloor\times\frac{k}{2}}\times\sum_{i=max\left(0,n\%k-\frac{k}{2}\right)}^{\min\left(n\%k,\frac{k}{2}\right)}C_{n\%k}^i\times C_{k-n\%k}^{\frac{k}{2}-i}\times 2^i \]

#include<cstdio>
#include<algorithm>
typedef long long LL;
using namespace std;
/*===========================================*/

LL n;
int k;
const LL mod = 1e9 + 7;

LL q_pow(LL x, LL n)
{
	LL ans = 1;
	while (n)
	{
		if (n & 1) ans = ans * x % mod;
		x = x * x % mod;
		n >>= 1;
	}
	return ans;
}

long long jc[100100];//階層數組
long long inv[100100];//逆元數組
long long bas[100100];
void exgcd(long long a, long long b, long long& x, long long& y)
{
	if (b)
		x = 1, y = 0;
	else
	{
		exgcd(b, a % b, y, x);
		y -= a / b * x;
	}
}
long long pow(long long x, int n)
{
	long long result = 1;
	while (n)
	{
		if (n & 1)
			result = result * x % mod;
		n >>= 1;
		x = x * x % mod;
	}
	return result;
}
void pre()
{
	jc[0] = inv[0] = 1;
	jc[1] = 1;
	bas[0] = 1;
	bas[1] = 2;
	for (int i = 2; i <= 100000; ++i)
		jc[i] = jc[i - 1] * i % mod, bas[i] = bas[i - 1] * 2 % mod;
	inv[100000] = q_pow(jc[100000], mod - 2);
	for (int i = 99999; i >= 0; --i)
		inv[i] = inv[i + 1] * ((LL)i + 1LL) % mod;
	return;
}
long long C(int n, int m)
{
	return jc[n] * inv[m] % mod * inv[n - m] % mod;
}

int main(void)
{
	pre();

	int T;
	LL ans;
	scanf("%d", &T);
	while (T--)
	{
		scanf("%lld%d", &n, &k);
		if (k & 1)
		{
			printf("0\n");
			continue;
		}
		ans = 0;
		int tmp = n % k, end = min(tmp, k / 2);
		for (int i = max(0LL, n % k - k / 2); i <= end; ++i)
			ans = (ans + C(k - tmp, k / 2 - i) * C(tmp, i) % mod * bas[i] % mod) % mod;
		ans = ans * q_pow(2, n / k * (k / 2)) % mod;
		printf("%lld\n", ans);
	}

}

D(SAM+二分)

題目連接 B
⭐⭐⭐⭐⭐
題意:
如今有兩個字符串\(s\)\(t\)\(q\)次查詢,每次查詢給出一個 \(l,r\)\(s[l] \sim s[r]\)中有多少個\(t\)的子串

解析:

  1. 關鍵字在於子串數目種類查詢,能夠聯想到SAM(後綴自動機),而後就能夠發現能夠對字符串\(t\)構建一個SAM
  2. \(s\)放入SAM中進行檢測。用\(le\)表明從當前\(s[cur]\)向左比較的和\(t\)最長公共子串長度(這個串同時也是當前\(s\)的後綴子串)
  3. 因爲是後綴子串,能夠在SAM中進行檢測,每次若是上一個狀態\(last\)有以當前字符\(s[cur]\)的轉移函數,那麼說明\(le\)能夠增長1,不然,就沿着前綴連接,並更新\(le\)(也就是縮減了後綴子串的長度)
  4. 若是一直沒有遇到能夠轉移的狀態,直到初始狀態,那麼也退出循環
  5. ++le,用\(d\)數組記錄從當前位置開始,向左看最長公共串的起始位置,而且再維護一個關於\(le\)前綴和數組\(sum\)
  6. 這時來看\(s\)所對應的某個區間\([l,r]\)

\[\underbrace{\overbrace{l(d[L]) \cdots}^{Part_1}\ \ \overbrace{L\cdots r}^{Part_2}}_{substr\in s} \]

\[result = part_1+part_2 \]

  1. 對於\(d\)數組,必定存在一個\(L\),使得\(d[L]=l\),因爲字符串長度逐漸增長,對於每一個\(i\)而言,匹配到相同長度後綴子串更加的困難,所以 \(d[i]\le d[j]\wedge i\le j\),因此能夠二分查找\(L\)的位置
    Part1. 這部分的全部子串都出現過,所以是等差數列求和便可
    Part2. 對於\(i\in [L,r]\)\(d[i]\)長度內的全部子串都出現過(因爲\(d[i]\)表明的是最遠位置),所以這部分結果爲\(\sum_{i=L}^rd[i]=sum[r]-sum[L-1]\)

總結:維護\(t\)的SAM,用SAM檢測\(s\),記錄每一個位置向左看最長的公共子串的起始位置以及對應長度的前綴和

\[ans=\frac{(L-l+1)\times (L-i)}{2}+sum[r]-sum[L-1] \]

吐槽:學了兩天後綴自動機,覺着轉換函數是對某個子串的拓展操做,且保證這個拓展是後綴子串,而前綴連接就至關於對前綴子串前半部分的刪除操做,一加一減讓SAM能夠獲取關於子串的信息,而不只僅是後綴串。感受這個算法真挺難的,並且應用還不太會,如今只是看題解會了這道題,後續繼續學習相關算法叭……

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

int d[75005], sum[75005];
char s1[75005], s2[75005];

struct SAM
{
	int size, last;
	vector<int> len, link;
	vector< vector<int>> to;

	void init(int strLen = 0, int chSize = 0)
	{
		strLen *= 2;
		size = last = 1;
		len.assign(strLen, 0);
		link.assign(strLen, 0);
		to.assign(strLen, vector<int>(chSize, 0));
		link[1] = len[1] = 0;
	}

	void extend(int c)
	{
		int p, cur = ++size;
		len[cur] = len[last] + 1;
		//狀況1
		for (p = last; p && !to[p][c]; p = link[p])
			to[p][c] = cur;
		if (!p) link[cur] = 1;
		else
		{
			int q = to[p][c];
			//A類
			if (len[q] == len[p] + 1)
				link[cur] = q;
			else //B類
			{
				int clone = ++size;
				len[clone] = len[p] + 1;
				link[clone] = link[q], to[clone] = to[q];
				while (p && to[p][c] == q)
					to[p][c] = clone, p = link[p];
				link[cur] = link[q] = clone;
			}
		}
		last = cur;
	}

	void solve()
	{
		int le = 0, p = 1, c;
		for (int i = 1; s1[i]; ++i)
		{
			c = s1[i] - 'a';
			while (p != 1 && !to[p][c])
				p = link[p], le = len[p];
			if (to[p][c])
				++le, p = to[p][c];
			d[i] = i - le + 1;
			sum[i] = sum[i - 1] + le;
		}
	}
}sol;

int main()
{
	freopen("curse.in", "r", stdin);
	int T;
	int q;
	scanf("%d", &T);
	for (int cas = 1; cas <= T; ++cas)
	{
		scanf("%s%s", s1 + 1, s2 + 1);
		sol.init(strlen(s2 + 1), 26);
		for (int i = 1; s2[i]; ++i)
			sol.extend(s2[i] - 'a');
		sol.solve();
		scanf("%d", &q);
		printf("Case %d:\n", cas);
		while (q--)
		{
			int l, r, L, R;
			scanf("%d%d", &l, &r);
			L = l, R = r;
			while (L < R)
			{
				int mid = L + (R - L) / 2;
				if (d[mid] < l)
					L = mid + 1;
				else
					R = mid;
			}
			printf("%lld\n", 1LL * sum[r] - sum[L - 1] + 1LL * (L - l + 1) * (L - l) / 2);
		}
	}
}

E(思惟+分治)

題目連接 H
⭐⭐⭐
題意:
給定區間\([L,R]\),求出知足下列條件的區間內最大的數

  1. 將數\(x\)按10進制位劃分爲前\(\left\lfloor\frac{bits}{2}\right\rfloor\)位,記做pre;後\(\left\lceil\frac{bits}{2}\right\rceil\),記做past
  2. 要求\(gcd(pre,past)>1\ \wedge\ past\ne 0\)

解析:

  1. 最樸素的思想必然是直接從L到R進行拆分,但在\(10^{13}\)的數據下必定會TLE,考慮分治作法

  2. 因爲要求\(gcd\ne 1\),因此能夠考慮一方爲素數的狀況,若是考慮past爲素數,則修改pre的狀況下,改動範圍太大,會出現錯誤,所以考慮pre是否爲素數的狀況

    對於每一個數當前數\(n\)

    1. \(pre是素數\wedge pre\le last\),很明顯最大的符合預期的數知足last爲pre的倍數\(n-past\%pre\)爲結果
    2. \(pre是素數\wedge pre> last\),這樣的話在\(past\in [0,past_{cur}]\),必定全爲與pre互素的數,此時只能改變pre,經過\(n=n-last-1\)實現
    3. 當pre不是素數時,樸素的用\(gcd\)進行判斷,但要注意\(past=0\)的狀況,這時候會直接返回pre,儘管知足條件2的前半部分,但後半部分並不知足
#include<cstdio>
#include<algorithm>
typedef long long LL;
using namespace std;
LL gcd(LL a, LL b) { return b ? gcd(b, a % b) : a; }

int pre, past;

void div(LL x)
{
	int ans = 0;
	LL t = x;
	while (t)
		t /= 10, ++ans;
	int mol = 1;
	ans = (ans + 1) / 2;
	while (ans--)
		mol *= 10;
	pre = x / mol;
	past = x % mol;
}

const int maxn = 1e7;
int cnt;
int prime[664579];
bool vis[maxn + 1];

void euler()
{
	vis[0] = vis[1] = true;
	for (LL i = 2; i <= maxn; ++i)
	{
		if (!vis[i]) prime[cnt++] = i;
		for (int j = 0; j < cnt && i * prime[j] <= maxn; ++j)
                {
			vis[i * prime[j]] = true;
                        if (i % prime[j] == 0) break;
                }
	}
}

int main(void)
{
	freopen("halfnice.in", "r", stdin);
	euler();
	int T;
	LL l, r;
	bool ok;
	scanf("%d", &T);
	for (int cas = 1; cas <= T; ++cas)
	{
		ok = false;
		scanf("%lld%lld", &l, &r);
		printf("Case %d: ", cas);
		while (r >= l)
		{
			div(r);
			if (!vis[pre])
			{
				if (pre <= past)
				{
					ok = true;
					printf("%lld\n", r - past % pre);
					break;
				}
				else
					r -= past + 1;
			}
			else
			{
				if (gcd(pre, past) != 1 && past)
				{
					ok = true;
					printf("%lld\n", r);
					break;
				}
				else
					--r;
			}
		}
		if (!ok)
			printf("impossible\n");
	}

}
相關文章
相關標籤/搜索