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");
	}

}
相关文章
相关标签/搜索