\(\text{T1 码灵鼠}\)
\(\text{Description}\)
对于数列 \(a\):
-
\(a_0 = 1\)
-
\(a_n = a_i + a_j(n\ge1,i,j\) 均在 \([0,n-1]\) 内均匀随机\()\)
请求出对于给定的 \(n\),\(a_n\) 的期望值是多少。
- \(0\le n\le2147483647\)。
\(\text{Solution}\)
你算一下前三项:
\(a_0=1\)
\(a_1=(a_0+a_0)\div1=2\)
\(a_2=[(a_0+a_0)+(a_0+a_1)+(a_1+a_0)+(a_1+a_1)]\div4=3\)
于是你就猜 \(a_n=n+1\)。
然后就对了(
现在我们来用数学归纳法证明一下(过程可能有不严谨之处,请见谅):
当 \(n=0\) 时,\(a_0=1\),成立。
当 \(\forall i\in[0,n-1],a_{i-1}=i\) 成立时,设 \(E(x)\) 为 \(x\) 的期望值,我们有
\(\begin{aligned}a_n&=E(a_i+a_J)(\forall i,j\in[0,n-1])\\&=E(a_i)+E(a_j)(\forall i,j\in[0,n-1])\\&=2\times E(a_i)(\forall i\in[0,n-1])\\&=2\times\dfrac{\sum_{i=0}^{n-1}a_i}{n}\\&=2\times\dfrac{\dfrac{1+2+\cdots+n}{2}}{n}\\&=n+1\end{aligned}\)
证毕。
记得特判 \(n=2147483647\),\(n+1\) 爆 int
了(不然会丢 \(30\) 分)。
时间复杂度 \(\mathcal{O}(T)\)。
\(\text{Code}\)
#include <iostream>
#include <cstdio>
using namespace std;
int main()
{
int t;
scanf("%d", &t);
while (t--)
{
int n;
scanf("%d", &n);
if (n == 2147483647)
{
puts("2147483648");
}
else
{
printf("%d\n", n + 1);
}
}
return 0;
}
\(\text{T2 So many prefix?}\)
\(\text{Description}\)
给定一个字符串 \(S\),请求出所有长度为偶数的 \(S\) 的前缀在 \(S\) 中出现的次数和。
- 对于 \(30\%\) 的数据,\(|S|\le100\),保证数据随机;
- 对于 \(100\%\) 的数据,\(|S|\le200000\)。
\(\text{Solution}\)
提供 \(2\) 种思路,剩下一种请咨询 xhj:
\(1. \rm{AC}\) 自动机 \(+\) 拓扑建图优化
只考虑偶数的话就和 LUOGU5357 【模板】AC自动机(二次加强版) 差不多了。
对于偶数位上的都给 \(pos\) 打上标记,然后 \(\operatorname{search}\) 时遇上有标记的就不停地跳 \(fail\) 同时将 ans++
。
这个的时间复杂度是 \(\mathcal{O}(|S|^2)\) 的。
那么加一个拓扑建图优化即可。
时间复杂度 \(\mathcal{O}(|S|)\)。
\(2. \rm{KMP+\rm{DP}}\)
设 \(dp_i\) 为长度为 \(i\) 的前缀的答案。
那么显然有 \(dp_i=\begin{cases}dp_{nxt_i}+1&i\equiv0\pmod{2}\\dp_{nxt_i}&i\equiv1\pmod{2}\end{cases}\)
所以 \(\operatorname{for}\) 一遍边推边求和即可。
时间复杂度 \(\mathcal{O}(|S|)\)。
\(\text{Code}\)
\(1.\)
#include <iostream>
#include <cstdio>
#include <cstring>
#include <queue>
using namespace std;
const int MAXN = 2e5 + 5;
int len, tot, cnt;
int trie[MAXN][26], id[MAXN], fail[MAXN], head[MAXN], siz[MAXN];
struct edge
{
int to, nxt;
}e[MAXN];
void add(int u, int v)
{
e[++cnt] = edge{v, head[u]};
head[u] = cnt;
}
void insert(char *s)
{
int pos = 0;
for (int i = 0; i < len; i++)
{
int c = s[i] - 'a';
if (!trie[pos][c])
{
trie[pos][c] = ++tot;
}
pos = trie[pos][c];
if (i & 1)
{
id[(i + 1) >> 1] = pos;
}
}
}
void build()
{
queue<int> q;
for (int c = 0; c < 26; c++)
{
if (trie[0][c])
{
q.push(trie[0][c]);
}
}
while (!q.empty())
{
int u = q.front();
q.pop();
for (int c = 0; c < 26; c++)
{
int v = trie[u][c];
if (v)
{
fail[v] = trie[fail[u]][c];
q.push(v);
}
else
{
trie[u][c] = trie[fail[u]][c];
}
}
}
}
void search(char *s)
{
int pos = 0, res = 0;
for (int i = 0; i < len; i++)
{
int c = s[i] - 'a';
pos = trie[pos][c];
siz[pos]++;
}
for (int i = 1; i <= tot; i++)
{
add(fail[i], i);
}
}
void dfs(int u)
{
for (int i = head[u]; i; i = e[i].nxt)
{
int v = e[i].to;
dfs(v);
siz[u] += siz[v];
}
}
char s[MAXN];
int main()
{
scanf("%s", s);
len = strlen(s);
insert(s);
build();
search(s);
dfs(0);
int ans = 0;
for (int i = 1; i <= (len >> 1); i++)
{
ans += siz[id[i]]; //只对偶数求和
}
printf("%d\n", ans);
return 0;
}
\(2.\)
#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
const int MAXN = 2e5 + 5;
char s[MAXN];
int nxt[MAXN], dp[MAXN];
int main()
{
scanf("%s", s + 1);
int len = strlen(s + 1);
for (int i = 2, j = 0; i <= len; i++)
{
while (j && s[i] != s[j + 1])
{
j = nxt[j];
}
if (s[i] == s[j + 1])
{
j++;
}
nxt[i] = j;
}
int ans = 0;
for (int i = 1; i <= len; i++)
{
dp[i] = dp[nxt[i]] + !(i & 1);
ans += dp[i];
}
printf("%d\n", ans);
return 0;
}
\(\text{T3 TRAVEL}\)
\(\text{Description}\)
在一张无向图上有 \(N\) 个点和 \(M\) 条边,每条边连接着点 \(a\) 和点 \(b\),有无数个人编号为 \(1,2,\dots,+\infty\)。通过某条边是要一定的条件的,对于每条边,有两个值 \(l\) 和 \(r\),表示该条边只允许编号在 \([l,r]\) 区间内的人通过,从 \(1\) 号点到达 \(N\) 号点,请求出最多有多少人可以通过,并求出这些人的编号(若有多组解取字典序最小的一组 )
- 可能有重边和自环。
- 如果没有人能通过只要输出
0
就可以了。 - 对于 \(30\%\) 的数据,\(1\le N,M\le10\);
- 对于 \(100\%\) 的数据,\(2\le N\le1000,0\le M\le3000,1\le a,b\le N,1\le l\le r\le10^6\)。
\(\text{Solution}\)
依然有两种思路(注意字典序最小的判断):
\(1.\) 最长路
遍历每一条边,对于第 \(i\) 条边,选取边权 \(\ge l_i\) 的边,从 \(1\) 到 \(N\) 跑最长路,让 \(r\) 最大,在 \(l\) 固定的情况下,肯定是 \(r\) 越大越好。
时间复杂度 \(\mathcal{O}(M\cdot M\log N)=\mathcal{O}(M^2\log N)\)。
\(2.\) 并查集
同样是遍历每一条边 \(i\),将所有满足 \(l_j\le l_i\) 的 \(j\),都把 \(a_j\) 和 \(b_j\) 并入一个集合。
那么最终的 \(l\) 一定是 \(l_i\),所以把所有的 \(r_i\) 取个 \(\min\) 作为最终的 \(r\),当点 \(1\) 和点 \(N\) 在同一个集合内说明可以尝试去更新答案。
因为是并查集实现,结合路径压缩和按秩合并可以做到 \(\mathcal{O}(M^2\alpha(N))\),不过因为 \(N\le1000\) 所以优化不大,主要优化在于常数(
时间复杂度 \(\mathcal{O}(M^2\alpha(N))\)。
\(\text{Code}\)
\(1.\)
#include <iostream>
#include <cstdio>
#include <queue>
#include <cstring>
using namespace std;
const int MAXN = 1005;
const int MAXM = 3005;
const int INF = 0x3f3f3f3f;
int cnt;
int head[MAXN];
struct edge
{
int to, l, r, nxt;
}e[MAXM << 1];
void add(int u, int v, int l, int r)
{
e[++cnt] = edge{v, l, r, head[u]};
head[u] = cnt;
}
int dis[MAXN];
bool vis[MAXN];
struct node
{
int u, r;
bool operator <(const node &x)const
{
if (x.r == r)
{
return x.u < u;
}
return x.r > r;
}
};
void init()
{
memset(dis, -INF, sizeof(dis));
memset(vis, false, sizeof(vis));
}
void dijkstra(int l)
{
init();
dis[1] = INF;
priority_queue<node> pq;
pq.push(node{1, dis[1]});
while (!pq.empty())
{
int u = pq.top().u;
pq.pop();
if (vis[u])
{
continue;
}
vis[u] = true;
for (int i = head[u]; i; i = e[i].nxt)
{
if (e[i].l > l) //取在区间内的
{
continue;
}
int v = e[i].to;
if (dis[v] < min(dis[u], e[i].r)) //r要取min
{
dis[v] = min(dis[u], e[i].r);
pq.push(node{v, dis[v]});
}
}
}
}
int l[MAXM], r[MAXM];
int main()
{
int n, m;
scanf("%d%d", &n, &m);
for (int i = 1; i <= m; i++)
{
int u, v;
scanf("%d%d%d%d", &u, &v, l + i, r + i);
add(u, v, l[i], r[i]);
add(v, u, l[i], r[i]);
}
int ansk = 0, ansl = 0, ansr = 0;
for (int i = 1; i <= m; i++)
{
dijkstra(l[i]);
int res = dis[n] - l[i] + 1;
if (ansk < res || (ansk == res && ansl > l[i]))
{
ansk = res;
ansl = l[i];
ansr = dis[n];
}
}
if (ansk)
{
printf("%d\n", ansk);
for (int i = ansl; i <= ansr; i++)
{
printf("%d ", i);
}
}
else
{
puts("0");
}
return 0;
}
\(2.\)
#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;
const int MAXN = 1005;
const int MAXM = 3005;
const int INF = 0x3f3f3f3f;
struct edge
{
int u, v, l, r;
}e[MAXM];
bool cmp(edge x, edge y)
{
if (x.r == y.r)
{
return x.l < y.l;
}
return x.r > y.r;
}
int n, m, r, ansk, ansl, ansr;
int fa[MAXN], siz[MAXN];
void init()
{
for (int i = 1; i <= n; i++)
{
fa[i] = i;
siz[i] = 1;
}
}
int find(int x)
{
if (x == fa[x])
{
return x;
}
return fa[x] = find(fa[x]); //路径压缩
}
int main()
{
scanf("%d%d", &n, &m);
for (int i = 1; i <= m; i++)
{
scanf("%d%d%d%d", &e[i].u, &e[i].v, &e[i].l, &e[i].r);
}
sort(e + 1, e + m + 1, cmp);
for (int i = 1; i <= m; i++)
{
init();
r = INF;
for (int j = 1; j <= m; j++)
{
if (e[j].l <= e[i].l)
{
int x = find(e[j].u), y = find(e[j].v);
if (x != y)
{
if (siz[x] > siz[y]) //按秩合并
{
fa[y] = x;
siz[x] += siz[y];
}
else
{
fa[x] = y;
siz[y] += siz[x];
}
r = min(r, e[j].r);
if (find(1) == find(n))
{
break;
}
}
}
}
if (find(1) == find(n) && (ansk < r - e[i].l + 1 || (ansk == r - e[i].l + 1 && ansl > e[i].l)))
{
ansk = r - e[i].l + 1;
ansl = e[i].l;
ansr = r;
}
}
if (ansk)
{
printf("%d\n", ansk);
for (int i = ansl; i <= ansr; i++)
{
printf("%d ", i);
}
}
else
{
puts("0");
}
return 0;
}
\(\text{T4 潜入行动}\)
\(\text{Description}\)
有一棵 \(n\) 个节点、\(n−1\) 条边的树,如果在节点 \(u\) 上安装监听设备,则J能够监听与 \(u\) 直接相邻所有的节点。特别注意放置在节点 \(u\) 的监听设备并不监听 \(u\) 本身。一共有了 \(k\) 个监听设备,请求出有多少种不同的放置监听设备的方法,能够使得树上所有节点都被监听。
-
每个节点至多只能安装一个监听设备,且监听设备必须被用完。
-
输出答案 \(\mod1000000007\) 的余数。
-
存在 \(10\%\) 的数据,\(1\le n\le20\);
-
存在另外 \(10\%\) 的数据,\(1\le n\le100\);
-
存在另外 \(10\%\) 的数据,\(1\le k\le10\);
-
存在另外 \(10\%\) 的数据,输入的树保证是一条链;
-
对于所有数据,\(1\le n\le105\),\(1\le k\le \min\{n,100\}\) 。
\(\text{Solution}\)
这种题一看就是 \(\rm DP\)。
设 \(dp_{u,x,0/1,0/1}\) 表示节点 \(u\) 的子树内有 \(x\) 个监听设备,且节点 \(u\) 是否安了监听设备,节点 \(u\) 是否被监听。
然后就是推柿子了(以下省略过程)设 \(v\) 为 \(u\) 的儿子。
\(dp_{u,j+l,0,0}=dp_{u,j,0,0}\times dp_{v,l,0,1}\)
\(dp_{u,j+l,0,1}=dp_{u,j,0,1}\times(dp_{v,l,0,1}+dp_{v,l,1,1})+dp_{u,j,0,0}\times dp_{v,l,1,1}\)
\(dp_{u,j+l,1,0}=dp_{u,j,1,0}\times(dp_{v,l,0,0}+dp_{v,l,0,1})\)
\(dp_{u,j+l,1,1}=dp_{u,j,1,0}\times(dp_{v,l,1,0}+dp_{v,l,1,1})+dp_{u,j,1,1}\times(dp_{v,l,0,0}+dp_{v,l,0,1}+dp_{v,l,1,0}+dp_{v,l,1,1})\)
注意事项:
- \(dp_{u,j,0/1,0/1}\) 这个要预先存起来,不然用来更新 \(dp_{u,j+l,0/1,0/1}\) 的不是上一轮的了。;
- 直接开
long long
会 \(\rm MLE\)。需要在计算时乘上1LL
。
\(\text{Code}\)
#include <iostream>
#include <cstdio>
using namespace std;
const int MAXN = 1e5 + 5;
const int MOD = 1e9 + 7;
int n, k, cnt;
int head[MAXN], siz[MAXN], dp[MAXN][105][2][2], tmp[105][2][2];
struct edge
{
int to, nxt;
}e[MAXN << 1];
void add(int u, int v)
{
e[++cnt] = edge{v, head[u]};
head[u] = cnt;
}
void dfs(int u, int fa)
{
siz[u] = dp[u][0][0][0] = dp[u][1][1][0] = 1; //初始化
for (int i = head[u]; i; i = e[i].nxt)
{
int v = e[i].to;
if (v == fa)
{
continue;
}
dfs(v, u);
for (int j = 0; j <= min(siz[u], k); j++)
{
tmp[j][0][0] = dp[u][j][0][0], dp[u][j][0][0] = 0;
tmp[j][0][1] = dp[u][j][0][1], dp[u][j][0][1] = 0;
tmp[j][1][0] = dp[u][j][1][0], dp[u][j][1][0] = 0;
tmp[j][1][1] = dp[u][j][1][1], dp[u][j][1][1] = 0;
}
for (int j = 0; j <= min(siz[u], k); j++)
{
for (int l = 0; l <= min(siz[v], k - j); l++)
{
dp[u][j + l][0][0] = (dp[u][j + l][0][0] + 1LL * tmp[j][0][0] * dp[v][l][0][1] % MOD) % MOD;
dp[u][j + l][0][1] = (dp[u][j + l][0][1] + 1LL * tmp[j][0][1] * (1LL * dp[v][l][0][1] + 1LL * dp[v][l][1][1]) % MOD + 1LL * tmp[j][0][0] * dp[v][l][1][1] % MOD) % MOD;
dp[u][j + l][1][0] = (dp[u][j + l][1][0] + 1LL * tmp[j][1][0] * (1LL * dp[v][l][0][0] + 1LL * dp[v][l][0][1]) % MOD) % MOD;
dp[u][j + l][1][1] = (dp[u][j + l][1][1] + 1LL * tmp[j][1][0] * (1LL * dp[v][l][1][0] + 1LL * dp[v][l][1][1]) % MOD + 1LL * tmp[j][1][1] * (1LL * dp[v][l][0][0] + 1LL * dp[v][l][0][1] + 1LL * dp[v][l][1][0] + 1LL * dp[v][l][1][1]) % MOD) % MOD;
}
}
siz[u] += siz[v];
}
}
int main()
{
scanf("%d%d", &n, &k);
for (int i = 1; i < n; i++)
{
int u, v;
scanf("%d%d", &u, &v);
add(u, v);
add(v, u);
}
dfs(1, 0);
printf("%d\n", (dp[1][k][0][1] + dp[1][k][1][1]) % MOD); //最终1号点必须被监视
return 0;
}