2020.11.20 选拔赛题解
A (思维)
题目链接
⭐⭐
题意:
给出下述两种操作
现在给出\(a\)与\(b\),问使得1通过上述操作变成\(\frac{a}{2^b}\)的最小长度指令序列(假设整数位的右边就是对应的小数位)
保证\(a\)是奇数
解析:
-
观察原公式,无非是把\(a\)化成2进制,再将他向右边移动\(n\)位。由于数据条件的限制,这个数一定是一个小数。这样就把问题转化成如何在小数部分凑出\(a\)所对应的二进制序列了。
观察操作指令,如果想要获得一个连续为1的序列例如获得0.111,可以先将1右移3位变成0.001,再通过1-x,变成所需要的0.111
-
可以发现这个过程中最后一位是保持不变的1,而之前产生的连续0因为1-x产生了反转变成了连续1。在这样的基础上如果再次左移,就可以获得连续的0,如此交替往复,我们似乎可以得到任何想要的二进制序列,也就达到了要求
-
对于最后一个出现的0(1),也就是第一位来说,它一定是1,所以当将这一位移动到小数部分时(也就是循环结束时),必须要进行1操作
-
最后输出剩余的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的数量,求方案数
解析:
- 由公式可以看出A和B是等价的,所以可以将A和B看成同一种,记作D。
- 对于连续\(k\)个字符要满足条件,可以看作\(k\)在区间内滑动,每次丢弃左端点的字符,一定得在右端点添加相同类型的字符,这样的情况下,如果确定了\(k\)中字符的字符排列,那么整体的类型布局也就确定了
- 问题转化成对于每种\(k\)区间内布局,整个\(n\)区间中有多少个D
通过所给公式可知,在\(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}\),同时要注意越界溢出情况,即
再根据加法原理,可以得到后半部分对应的公式
总结:
#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\)的子串
解析:
- 关键字在于子串数目种类查询,可以联想到SAM(后缀自动机),然后就可以发现可以对字符串\(t\)构建一个SAM
- 将\(s\)放入SAM中进行检测。用\(le\)代表从当前\(s[cur]\)向左比较的和\(t\)的最长公共子串长度(这个串同时也是当前\(s\)的后缀子串)
- 由于是后缀子串,可以在SAM中进行检测,每次如果上一个状态\(last\)有以当前字符\(s[cur]\)的转移函数,那么说明\(le\)可以增加1,否则,就沿着前缀链接,并更新\(le\)(也就是缩减了后缀子串的长度)
- 如果一直没有遇到可以转移的状态,直到初始状态,那么也退出循环
++le
,用\(d\)数组记录从当前位置开始,向左看最长公共串的起始位置,并且再维护一个关于\(le\)前缀和数组\(sum\)- 这时来看\(s\)所对应的某个区间\([l,r]\)
- 对于\(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\),记录每个位置向左看最长的公共子串的起始位置以及对应长度的前缀和
吐槽:学了两天后缀自动机,觉着转换函数是对某个子串的拓展操作,且保证这个拓展是后缀子串,而前缀链接就相当于对前缀子串前半部分的删除操作,一加一减让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]\),求出满足下列条件的区间内最大的数
- 将数\(x\)按10进制位划分为前\(\left\lfloor\frac{bits}{2}\right\rfloor\)位,记作pre;后\(\left\lceil\frac{bits}{2}\right\rceil\),记作past
- 要求\(gcd(pre,past)>1\ \wedge\ past\ne 0\)
解析:
-
最朴素的思想必然是直接从L到R进行拆分,但在\(10^{13}\)的数据下一定会TLE,考虑分治做法
-
由于要求\(gcd\ne 1\),所以可以考虑一方为素数的情况,如果考虑past为素数,则修改pre的情况下,改动范围太大,会出现错误,因此考虑pre是否为素数的情况
对于每个数当前数\(n\)
- 当 \(pre是素数\wedge pre\le last\),很明显最大的符合预期的数满足last为pre的倍数,\(n-past\%pre\)为结果
- 当 \(pre是素数\wedge pre> last\),这样的话在\(past\in [0,past_{cur}]\),一定全为与pre互素的数,此时只能改变pre,通过\(n=n-last-1\)实现
- 当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");
}
}