1 循环位移
题意:给出一个模板串a,让a进行循环位移,即把最后一个字符放到第一个,如此反复知道重新变为原a,会得到多个不同的新字符,问在b里面最多能找到几个a或者变种a。
思路:一开始的思路很简单,map存string然后滑动窗口遍历b找即可。问题是map存string需要的复杂度太大了,而且滑动窗口对字符串也很难处理,要是都能转化成数字就好了,那么对于数字和字符的转换,我们很容易想到哈希算法,通过一个质数将字符串转换成大数字进行存储。
https://www.luogu.com.cn/problem/P3370 不知道的小伙伴可以去洛谷学习一下,还是很轻易就能理解的。
那么到这边我们已经可以解决第一个问题了,可以把所有的变种串和原串都存下来,然后再去思索如何快速进行滑动窗口呢?其实滑动窗口每次的变化也很好理解,就是把第一个删掉把下一个字符加上。
那么对于一个字符串xyz,我们想想,转化成哈希值应该是x*131的二次+y*131+z,如果我们想变成y*131的二次+131*z+p,改如何很快的变化呢?很容易发现,只需要把最左边的x的131二次去掉,然后对剩下的整体*131,在加上新的p即可。同理对于很长的子串,我们记录一下第一个字符的值和它对应的质数的次数,然后根据上面进行同样的变化就很好处理了,这边可以前缀和先算好质数的次数,到时候直接取用就好。
代码:
#include<iostream>
#include<map>
#include<string>
#include<vector>
#include<algorithm>
using namespace std;
#define ll long long
#define ull unsigned long long
#define endl '\n';
ull mod = 212370440130137957ll;
ull base = 131;
signed main()
{
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
ll t;
cin >> t;
while (t--)
{
map<ull, int> M;
string a;
string b;
cin >> a >> b;
ll n = a.size();
ll m = b.size();
a = ' ' + a;
b = ' ' + b;
vector<ull> p(m + 1); //前缀和快速计算base的i次
vector<ull> pre(m + 1); //前缀和快速计算hash值。
p[0] = 1;
for (int i = 1; i <= n; i++)
{
p[i] = p[i - 1] * base; //预处理前缀和
pre[i] = (pre[i - 1] * base + a[i]);//同上
}
ull sum = pre[n];
M[sum] = 1;
for (int i = 1; i < n; i++)
{
ull v = a[i];
sum = ((sum - (p[n - 1] * v))) * base + v; //快速处理需要存的字符串只需要处理一位即可,如131*131*x+131*y+z,可以通过(t-131*131*x)*131+x变成另一种可能的字符哈希,节省时间
M[sum] = 1;
}
ll ans = 0;
ull now = 0;
for (int i = 1; i <= n; i++)
{
now = (now * base + b[i]);
}
if (M[now])
{
ans++;
}
int j = 1;
for (int i = n + 1; i <= m; i++)
{
now = ((now - (p[n - 1] * b[j])) * base + b[i]); //同预处理哈希一样的思路,不过要记录一下左端点
j++;
if (M[now])
{
ans++;
}
}
cout << ans << endl;
}
}
2 星星
题意:小a有n次操作机会,每次操作机会有五种选择,不取,花费a取1星,花费b取2星,花费c取3星以及花费d取4星,问正好取到k的最小代价。
思路:很经典的dp,我们开个dp数组记录一下取i颗星星的最小值,然后从后往前更新dp值就行,为什么不从前往后更新呢,因为从前往后更新会导致在一轮多次操作,这显然是不可以的。
8 位运算
题意:给出一个n,问你有多少种abcd的取法使得(a&b)^c|d=n。
思路:既然有这么多二进制运算符,我们干脆直接把n转化成二进制来看,我们会发现,对于每一位n,当其等于0时,d的对应位置只可能是0,同时左边三位操作完后也得是0,那么再讨论左边三位操作完是0的可能组合,再细分成左边两位的值和c,最后讨论出来发现要让对应位置为0,可能的取法是4种,为1可能的取法是12种。然后答案就是每位的取法乘积,输出即可。
12 并
题意:给出n个矩阵,问你随机取k个矩阵的面积并的期望。
面积并:就是n块矩阵的面积和,重叠计算的部分要减去。
期望:就是比如一共5个矩形,我们取其中k个的面积并的期望的话,就是把所有取法的面积加起来然后除以所有取法的种类,就是期望。
思路:
对于一个被i个图形覆盖的地方来说,
要么它在那k个图形的并集里,要么它不在那k个图形的并集里面。
如果去求在k个图形并集里的概率过于复杂,我们要考虑在i个覆盖它的图形里面选,而且要考虑在i个覆盖里面选几个,在外面不覆盖的里面选几个,讨论情况太多了,舍去。
如果求它不在那k个图形的并集里面的概率,那么我们只要把i个图形去掉,因为i个图形覆盖了这个位置,那么去掉这i个图形后剩下的图形不管怎么选都和这块区域无关了,也就是讨论覆盖里取0,覆盖外取k个的概率,就只剩下这一种情况,那就非常好讨论了。
这样我们用1-不在里面的概率,就是这块地方在并集里的概率,然后再×这种的总面积,就是它对答案的贡献。
为什么是×总面积呢,因为我们算一块这样的地方,会得到一部分面积,但想算其他地方时候你发现,因为都是被i块矩形覆盖,最后概率是一样的,无非是最后×的面积不同罢了,我们不如直接×总面积不就好了,这样就又可以缩短时间复杂度。
最后的答案就是枚举i从1到n的贡献的和输出即可。
处理的时候可以使用扫描线思想或者离散化暴力分格子都可以。
代码:
#include<iostream>
#include<vector>
#include<cstdlib>
#include<algorithm>
#include<map>
#include<set>
using namespace std;
#define a first
#define b second
#define int long long
#define mod 998244353
int c[2005][2005];
void init() //组合数初始化
{
c[0][0] = c[1][0] = c[1][1] = 1;
for (int i = 2; i < 2005; i++)
{
c[i][0] = 1;
for (int j = 1; j <= i; j++)
{
c[i][j] = (c[i-1][j] + c[i - 1][j - 1]) % mod;
}
}
}
int qp(int x, int n) //快速幂,用于最后的乘法逆元
{
int ans = 1;
while (n)
{
if (n & 1) ans = (ans * x) % mod;
x = (x * x) % mod;
n >>= 1;
}
return ans;
}
struct xline //存与y轴平行的线
{
int x;
int y1;
int y2;
int flag;
};
struct yline //存与x轴平行的线
{
int y;
int x1;
int x2;
int flag;
};
int cmp1(xline a, xline b) //把竖线从左往右排
{
return a.x < b.x;
}
int cmp2(yline a, yline b) //把横线从下往上排
{
return a.y < b.y;
}
signed main()
{
init();
vector<xline> xarr;
vector<yline> yarr;
int n;
cin >> n;
for (int i = 1; i <= n; i++)
{
int x1, y1, x2, y2;
cin >> x1 >> y1 >> x2 >> y2;
swap(y1, y2); //注意,读题会发现y轴不是往上而是往下的,所以如果处理的时候是正常处理的话,要把y1和y2交换以下不然就会错。
// x1左,x2右边,y1上边,y2下边
xarr.push_back({ x1,y1,y2,1 });
xarr.push_back({ x2,y1,y2,0 });
yarr.push_back({ y2,x1,x2,1 });
yarr.push_back({ y1,x1,x2,0 });
}
sort(xarr.begin(), xarr.end(), cmp1);
sort(yarr.begin(), yarr.end(), cmp2);
vector<int> g(n + 1);
vector<int> tim(n + 1);
int mark1 = 0; //计算是不是第一条从左到右的边
int lowx = 0;
for (auto [x,y1,y2,f] : xarr) { //类似扫描线的思路,先从左到右扫一遍,再从下到上扫一遍
if (mark1) //第一条从左到右的边不用计算贡献,因为他左边没有边了,中间没有差值没必要计算
{
for (int i = 1; i <= n; i++)
{
g[i] += (tim[i] * (x - lowx)) % mod; //对于之后的每条边,我们都要把它和上一条边之间这个范围内的面积加到对应的被覆盖几次的面积上。
g[i] %= mod;
}
}
tim = vector<int>(n + 1); //tim[i]表示被覆盖了i次的区域的y轴总长度
lowx = x;
mark1 = 1;
int mark2 = 0; //记录从下往上第一条边
int lowy = 0; //记录上一条边的坐标,以便于计算矩形面积
int cnt = 0; //记录覆盖次数,根据flag的值判断是出边还是入边
for (auto [y, x1, x2, flag] : yarr)
{
if (x1 <= x && x < x2)
{
if (flag)
{
if (!mark2)
{
mark2 = 1;
lowy = y;
cnt++;
}
else
{
tim[cnt] += y - lowy;
tim[cnt] %= mod;
lowy = y;
cnt++;
}
}
else
{
tim[cnt] += y - lowy;
tim[cnt] %= mod;
cnt--;
lowy = y;
}
}
}
}
for (int k = 1; k <= n; k++)
{
int ans = 0;
int p = qp(c[n][k], mod - 2); //下面多次会用到p,每次都快速幂跑一遍会超时,先预处理好就行。
for (int i = 1; i <= n; i++)
{
ans += ((1 - (c[n - i][k]) * p % mod + mod) % mod)*g[i]; //根据推导式子输出
ans %= mod;
}
cout << ans % mod << endl;
}
}