总结:这场多校题目并不难,大概有7到8个可做题。也有几个是基本的套路,有几个需要思考一下。
1.Just Skip The Problem。答案n!。阶乘取模预处理即可。不再贴代码了。
2.Keen On Everything But Triangle。区间第k大问题,主席树。但是这里需要说明一点原因,假设我们发现区间前三大不能组成三角形,那么说明区间第一大一定是不能选的,这样一直递推下去即可。
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll inf = 1e9 + 10;
const int maxn = 2e5 + 5;
int ls[maxn * 30], rs[maxn * 30], siz[maxn * 30], tot, rt[maxn], n, m, a[maxn];//update用了几次,就要乘以多少
void ini() { tot = 0; }
void update(int pre, int& u, int l, int r, int pos, int val) {//把u按照pre复制,然后更新pos
u = ++tot;
ls[u] = ls[pre]; rs[u] = rs[pre];
siz[u] = siz[pre] + val;
if (l == r)return;
int mid = (l + r) >> 1;
if (pos <= mid) update(ls[pre], ls[u], l, mid, pos, val);
else update(rs[pre], rs[u], mid + 1, r, pos, val);
}
int query(int x, int y, int l, int r, int k)
{
//查询(y树-x树)差值的区间第k小
if (l == r)return l;
int s = siz[ls[y]] - siz[ls[x]];
int mid = (l + r) >> 1;
if (k <= s) return query(ls[x], ls[y], l, mid, k);
else return query(rs[x], rs[y], mid + 1, r, k - s);
}
int main()
{
while (cin >> n >> m)
{
ini();
for (int i = 1; i <= n; i++) scanf("%d", &a[i]), update(rt[i - 1], rt[i], 1, inf, a[i], 1);
for (int i = 1; i <= m; i++)
{
int l, r;
scanf("%d%d", &l, &r);
if (r - l + 1 < 3)
{
printf("-1\n");
continue;
}
ll b[3];
b[1] = query(rt[l - 1], rt[r], 1, inf, r - l + 2 - 1);
b[2] = query(rt[l - 1], rt[r], 1, inf, r - l + 2 - 2);
for (int j = 3; j <= r - l + 1; j++)
{
b[0] = b[1];
b[1] = b[2];
b[2] = query(rt[l - 1], rt[r], 1, inf, r - l + 2 - j);
if (b[0] >= b[1] + b[2]) continue;
else
{
printf("%lld\n", b[0] + b[1] + b[2]);
break;
}
}
if (b[0] >= b[1] + b[2]) puts("-1");
}
}
}
3.Everything Is Generated In Equal Probability。这个题很多人都是猜出来的????我大写的服气,这也能猜出来,果然是我菜了。其实推导也不难.
首先,我们假设序列的长度就是n,那么他会有多少个逆序对呢?C(n,2)个。那么,我们在n个数中任意选两个,方案是C(n,2)他们在序列中可以有C(n,2)个位置,然后剩下(n-2)个数随机排列。这样第一步的方案就是:C(n,2)*C(n,2)*(n-2)!.现在考虑这对逆序对的贡献,假设它的贡献是:x.那么首先这对点被选中的概率是 1/4.在下一次递归时,他们被保留地概率是:1/4,如果被保留,就会再一次产生贡献,贡献+1.否则没有贡献,这样就会得到方程:x=(1+x)*1/4+3/4.解出:x=4/3.序列长度为n的期望就是
C(n,2)*C(n,2)*(n-2)!*(4/3)/n!.
最后除全排列,那么他被选中的概率是1/n.所以最后的式子就是:
可以继续化简,也可以直接写了,因为n很小。
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll mod = 998244353;
const int N = 1e4 + 10;
ll fac[N];
void init()
{
fac[0] = 1;
for (int i = 1; i < N; i++)fac[i] = fac[i - 1] * i% mod;
}
ll qpow(ll a, ll b)
{
ll res = 1;
while (b)
{
if (b & 1)res = res * a % mod;
a = a * a % mod;
b >>= 1;
}
return res;
}
ll getC(ll i)
{
ll ans = (i - 1) * i % mod;
ans /= 2;
ans = ans * ans % mod;
return ans;
}
ll getans(ll n)
{
ll ans = 0;
ll inv = qpow(3, mod - 2);
for (int i = 1; i <= n; i++)
{
ll tem = getC(i) * fac[i - 2] % mod * 4 % mod * inv % mod * qpow(fac[i], mod - 2) % mod;
ans = ans + tem;
ans %= mod;
}
ans = ans * qpow(n, mod - 2) % mod;
return ans;
}
int main()
{
ll n;
init();
while (cin>>n)
{
cout << getans(n) << endl;
}
}
4.I Love Palindrome String。回文自动机+Hash。首先,根据字符串建立回文自动机,由于回文自动机上的每一个节点都是一个回文串,并且儿子和父亲的区别仅仅是两边的字母,那奇回文举例,我们从1号点搜索下去,对于每个节点,正反Hash一次,如果发现正反hash值相同,那么就符合条件,统计答案,继续向下搜索。这样当dfs结束时,就可以把所有的回文串给统计完。
#include<bits/stdc++.h>
using namespace std;
typedef unsigned long long ull;
struct str_hash {//单hash
static const ull maxn = 3e5 + 666, p = 47, mod = 1e9 + 7;
static ull pw[maxn];
ull hash[maxn], revhash[maxn], len;
str_hash() {
if (pw[0] == 1)return;
pw[0] = 1;
for (ull i = 1; i < maxn; i++) {
pw[i] = pw[i - 1] * p % mod;
}
}
void ini() { len = 0; }
void add(char ch) {
// cout<<"add" <<ch<<endl;
len++;
hash[len] = (hash[len - 1] * p + ch - 'a') % mod;
revhash[len] = (revhash[len - 1] + pw[len - 1] * (ch - 'a')) % mod;
}
void del() {
// cout<<"del"<<endl;
len--;
}
bool ok() {
return hash[len] == revhash[len];
}
//注意没有下标检查
};
ull str_hash::pw[maxn];
struct palindrome_tree {
static const int maxn = 3e5 + 666;
int trans[maxn][26], len[maxn], suf[maxn], num[maxn], ans[maxn];
//字典树一样,len代表回文长度,suf指向回文后缀,类似于fail指针,num是最长后缀的数量,经过calc之后是后缀数量
int last, cnt;//last是上一个回文后缀,cnt是所有节点数
str_hash hashmation;
int new_node(int _len, int _suf, int _num) {//长度,后缀,数量
for (int i = 0; i < 26; i++)trans[cnt][i] = 0;
len[cnt] = _len;
suf[cnt] = _suf;
num[cnt] = _num;
return cnt++;
}
void ini() {
cnt = 0;
int root_even = new_node(0, 1, 0);//=1
int root_odd = new_node(-1, 1, 0);//=0
last = root_odd;
}
void build(char* s, int lens) {//s是要建立回文自动机的字符串,下标从1开始
for (int i = 0; i <= lens; i++) ans[i] = 0;
for (int i = 1; i <= lens; i++)extend(s, i);
calc();
}
void extend(char* s, int cur) {
int w = s[cur] - 'a';//当前结点的值
int p = last;//上一次匹配到的回文后缀
while (s[cur - len[p] - 1] != s[cur]) p = suf[p];
//现在p结点的cur儿子,要么是匹配成功的最长非自身回文后缀,要么是自身这一个字符
if (!trans[p][w]) {//如果此回文后缀未出现过,要创建节点
int v = suf[p];//我们来找他的suffix link回边,
while (s[cur - len[v] - 1] != s[cur])v = suf[v];
//此时意味着找到了suffix link 是v的儿子
trans[p][w] = new_node(len[p] + 2, trans[v][w], 0);
}
last = trans[p][w];//这一次匹配到的回文后缀
num[last]++;
}
void calc() {
for (int i = cnt - 1; ~i; i--)num[suf[i]] += num[i];//结点创建顺序保证了suf[i]<i
}
void solve()
{
hashmation.ini();
dfs(1);
hashmation.ini();
dfs(0);
}
void dfs(int rt)
{
for (int i = 0; i < 26; i++)
{
if (trans[rt][i] != 0)
{
hashmation.add('a' + i);
if (hashmation.ok())
{
ans[len[trans[rt][i]]] += num[trans[rt][i]];
}
dfs(trans[rt][i]);
hashmation.del();
}
}
}
}pat;
const int maxn = 3e5 + 666;
char s[maxn];
int main() {
while (~scanf("%s", s + 1)) {
int len = strlen(s + 1);
pat.ini();
pat.build(s, len);
pat.solve();
for (int i = 1; i <= len; i++) {
printf("%d", pat.ans[i]);
if (i != len) printf(" ");
}
printf("\n");
}
}
5.后边的题目时队友搞的,还没写。
1012当时写了一个分治,每次用出现次数小于k的字符分割区间,然后T了。。。。。。,队友线段树写过了。
1002是一个很常见的DP,队友说树状数组搞一下即可。
1004上来看了我大概就感受到了这是一个边分治。
1008有技巧的网络流,暂时还没啥思路。