HDU2222:Keywords Search 题解

思路

这道题就是一道 AC 自动机的模板题。AC 自动机适用于多个模式串匹配的情景,实际上,它就是在 Trie 树上进行的一个匹配算法。这里有一篇很优秀的讲稿,不了解 AC 自动机的读者可以自行阅读:https://blog.csdn.net/creatorx/article/details/71100840

这道题我们只需要在 Trie 树上增加一个关键字 \(sum\) 即可。

代码

// HDU-2222.cpp
#include <iostream>
#include <queue>
#include <cstdio>
#include <cstring>
using namespace std;
struct node
{
    node *nxt[26], *fail;
    int sum;
    ~node()
    {
        for (int i = 0; i < 26; i++)
            if (nxt[i] != NULL)
                delete nxt[i];
    }
};
node *root;
int cnt;
char str[1000020];
void insert(string str)
{
    int siz = str.length();
    node *p = root;
    for (int i = 0; i < siz; i++)
    {
        int curt = str[i] - 'a';
        if (p->nxt[curt] == NULL)
        {
            p->nxt[curt] = new node();
            p->nxt[curt]->fail = NULL;
            for (int i = 0; i < 26; i++)
                p->nxt[curt]->nxt[i] = NULL;
            p->nxt[curt]->sum = 0;
        }
        p = p->nxt[curt];
    }
    p->sum++;
}
void build_AC_automation()
{
    queue<node *> q;
    q.push(root);
    node *p, *tmp;
    while (!q.empty())
    {
        tmp = q.front();
        q.pop();
        for (int i = 0; i < 26; i++)
            if (tmp->nxt[i])
            {
                if (tmp == root)
                    tmp->nxt[i]->fail = root;
                else
                {
                    p = tmp->fail;
                    while (p)
                        if (p->nxt[i])
                        {
                            tmp->nxt[i]->fail = p->nxt[i];
                            break;
                        }
                        else
                            p = p->fail;
                    if (p == NULL)
                        tmp->nxt[i]->fail = root;
                }
                q.push(tmp->nxt[i]);
            }
    }
}
void ac_automation(string passage)
{
    int siz = passage.length();
    node *p = root;
    for (int i = 0; i < siz; i++)
    {
        int curt = passage[i] - 'a';
        while (!p->nxt[curt] && p != root)
            p = p->fail;
        p = p->nxt[curt];
        if (p == NULL)
            p = root;
        node *tmp = p;
        while (tmp != root)
        {
            if (tmp->sum >= 0)
            {
                cnt += tmp->sum;
                tmp->sum = -1;
            }
            else
                break;
            tmp = tmp->fail;
        }
    }
}
int main()
{
    int T;
    scanf("%d", &T);
    while (T--)
    {
        int n;
        scanf("%d", &n);
        root = new node();
        for (int i = 1; i <= n; i++)
            scanf("%s", str), insert(str);
        build_AC_automation();
        scanf("%s", str);
        ac_automation(str);
        printf("%d\n", cnt);
        cnt = 0;
    }
    return 0;
}

 

P2106:Sam 数题解

这道题算是一道矩阵递推的模板题吧。不是很了解矩阵乘法或者是矩阵递推加速原理的可以移步这篇 Wiki:矩阵 – OI Wiki.我们很快可以推出一个 DP 方程出来,其中\(i\)代表位数,\(j\)代表第一位所填的数:

\[dp[i][j] = \sum^{j+2}_{k=j-2}dp[i-1][k]\]

然后,我们可以根据这个来推出一个递推矩阵:

\[\left[ \begin{array}{ccc}
1 & 1 & 1 & 0 & 0 & 0 & 0 & 0 & 0 & 0\\
1 & 1 & 1 & 1 & 0 & 0 & 0 & 0 & 0 & 0\\
1 & 1 & 1 & 1 & 1 & 0 & 0 & 0 & 0 & 0\\
0 & 1 & 1 & 1 & 1 & 1 & 0 & 0 & 0 & 0\\
0 & 0 & 1 & 1 & 1 & 1 & 1 & 0 & 0 & 0\\
0 & 0 & 0 & 1 & 1 & 1 & 1 & 1 & 0 & 0\\
0 & 0 & 0 & 0 & 1 & 1 & 1 & 1 & 1 & 0\\
0 & 0 & 0 & 0 & 0 & 1 & 1 & 1 & 1 & 1\\
0 & 0 & 0 & 0 & 0 & 0 & 1 & 1 & 1 & 1\\
0 & 0 & 0 & 0 & 0 & 0 & 0 & 1 & 1 & 1\\
\end{array} \right]\]

初始矩阵是这样的:

\[\left[ \begin{array}{ccc}
0 & 1 & 1 & 1 & 1 & 1 & 1 & 1 & 1 & 1\\
\end{array} \right]\]

代码如下:

// P2106.cpp
#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
#define ll long long
using namespace std;
const ll mod = 1000000007;
struct matrix
{
    ll mat[10][10];
    matrix() { memset(mat, 0, sizeof(mat)); }
    matrix operator*(const matrix &ma) const
    {
        matrix empt;
        for (ll i = 0; i < 10; i++)
            for (ll j = 0; j < 10; j++)
                for (ll k = 0; k < 10; k++)
                    empt.mat[i][j] = (empt.mat[i][j] + mat[i][k] * ma.mat[k][j] % mod) % mod;
        return empt;
    }
    matrix operator^(const ll &num) const
    {
        ll tim = num;
        matrix pre = *(this);
        matrix tmp;
        for (ll i = 0; i < 10; i++)
            tmp.mat[i][i] = 1;
        while (tim)
        {
            if (tim & 1)
                tmp = tmp * pre;
            pre = pre * pre;
            tim >>= 1;
        }
        return tmp;
    }
} premat, Dmat;

int main()
{
    ll k;
    scanf("%lld", &k);
    if (k == 1)
    {
        printf("%lld", 10);
        return 0;
    }
    for (ll i = 1; i < 10; i++)
        premat.mat[0][i] = 1;
    for (ll i = 0; i < 10; i++)
        for (ll j = i - 2; j <= i + 2; j++)
        {
            if (j < 0)
                continue;
            if (j > 9)
                break;
            Dmat.mat[j][i] = 1;
        }
    Dmat = Dmat ^ (k - 1);
    premat = premat * Dmat;
    ll ans = 0;
    for (ll i = 0; i < 10; i++)
        ans += premat.mat[0][i], ans %= mod;
    printf("%lld", ans);
    return 0;
}

P3233:「HNOI2014」世界树题解

思路

这道题对初学虚树的人来讲简直就是噩梦…之前打了一个 30 分暴力未果,遂转向题解。知道了一个叫做虚树的东西,然后抄了一下午的题解才 AC。

我先来解释一下虚树这个数据结构(?)。在一棵书上,会有关键点集和非关键点集,在一些问题中我们只需要用到关键点之间的关系,而非关键点集便不在那么重要。这个时候我们可以建立一个虚树。

建立虚树的过程相当之繁琐,我在这里不在详细讲,可以使用单调栈和 LCA 算法以极高的效率搞定。详细见:https://oi-wiki.org/ds/virtual-tree/

以下为代码:

// P3233.cpp
#include <cstdio>
#include <iostream>
#include <cstring>
#include <vector>
#include <algorithm>
#define pr pair<int, int>
using namespace std;
const int MX_N = 300020;
int head[MX_N], current;
struct edge
{
    int to, nxt;
} edges[MX_N << 1];
int fa[MX_N], stfa[20][MX_N], n, dep[MX_N], anses[MX_N], id[MX_N], dfn = 0, q, m;
int tmpx, tmpy, st[MX_N], top = 1, tsiz[MX_N];
pr mx[MX_N];
bool vis[MX_N];
void addpath(int src, int dst)
{
    edges[current].to = dst, edges[current].nxt = head[src];
    head[src] = current++;
}
void preprocess()
{
    for (int i = 1; i <= n; i++)
        stfa[0][i] = fa[i];
    for (int tim = 1; tim < 20; tim++)
        for (int u = 1; u <= n; u++)
            stfa[tim][u] = stfa[tim - 1][stfa[tim - 1][u]];
}
int jump(int u, int p)
{
    for (int i = 0; i <= 19; i++)
        if ((p >> i) & 1)
            u = stfa[i][u];
    return u;
}
int getLca(int a, int b)
{
    // b is deeper;
    if (dep[a] > dep[b])
        swap(a, b);
    b = jump(b, dep[b] - dep[a]);
    if (a == b)
        return a;
    for (int tim = 19; tim >= 0; tim--)
        if (stfa[tim][a] != stfa[tim][b])
            a = stfa[tim][a], b = stfa[tim][b];
    return fa[a];
}
void dfs_fa(int u)
{
    id[u] = ++dfn;
    tsiz[u] = 1;
    dep[u] = dep[fa[u]] + 1;
    for (int i = head[u]; i != -1; i = edges[i].nxt)
        if (fa[u] != edges[i].to)
            fa[edges[i].to] = u, dfs_fa(edges[i].to), tsiz[u] += tsiz[edges[i].to];
}
bool compare(const int &a, const int &b) { return id[a] < id[b]; }
void dfs_1(int u)
{
    if (vis[u])
        mx[u] = make_pair(0, u);
    else
        mx[u] = make_pair(1e8, 0);
    for (int i = head[u]; i != -1; i = edges[i].nxt)
    {
        int to = edges[i].to;
        dfs_1(to);
        pr tmp = mx[to];
        tmp.first = dep[mx[to].second] - dep[u];
        mx[u] = min(mx[u], tmp);
    }
}
void dfs_2(int u)
{
    for (int i = head[u]; i != -1; i = edges[i].nxt)
    {
        pr p = mx[u];
        p.first += dep[edges[i].to] - dep[u];
        mx[edges[i].to] = min(mx[edges[i].to], p);
        dfs_2(edges[i].to);
    }
    anses[mx[u].second] = max(anses[mx[u].second], tsiz[u]);
}
void dfs_3(int u)
{
    for (int i = head[u]; i != -1; i = edges[i].nxt)
    {
        int x = mx[u].second, y = mx[edges[i].to].second;
        if (x != y)
        {
            int dist = dep[x] + dep[y] - (dep[getLca(x, y)] << 1);
            int z = jump(edges[i].to, (dist >> 1) - mx[edges[i].to].first);
            if (dist & 1)
                anses[x] -= tsiz[z];
            else
            {
                if (z != u && z != edges[i].to)
                    z = jump(edges[i].to, (dist >> 1) - mx[edges[i].to].first - (x < y));
                else if (z == u)
                    z = jump(edges[i].to, (dist >> 1) - mx[edges[i].to].first - 1);
                anses[x] -= tsiz[z];
            }
            if (edges[i].to != z)
                anses[y] += tsiz[z] - tsiz[edges[i].to];
        }
        dfs_3(edges[i].to);
    }
}
int main()
{
    memset(head, -1, sizeof(head));
    scanf("%d", &n);
    for (int i = 1; i < n; i++)
        scanf("%d%d", &tmpx, &tmpy), addpath(tmpx, tmpy), addpath(tmpy, tmpx);
    dfs_fa(1);
    preprocess();
    scanf("%d", &q);
    while (q--)
    {
        current = 0;
        scanf("%d", &m);
        vector<int> harr, arrs;
        for (int i = 1; i <= m; i++)
            scanf("%d", &tmpx), vis[tmpx] = true, harr.push_back(tmpx), anses[tmpx] = 0, arrs.push_back(tmpx);
        sort(harr.begin(), harr.end(), compare);
        // start to build the virtual tree;
        // prep for the stack;
        st[top = 1] = 1, head[1] = -1;
        for (int i = 0; i < m; i++)
        {
            if (harr[i] == 1)
                continue;
            int curtpt = harr[i], lca = getLca(curtpt, st[top]);
            if (lca != st[top])
            {
                while (id[lca] < id[st[top - 1]])
                    addpath(st[top - 1], st[top]), top--;
                if (id[lca] > id[st[top - 1]])
                    head[lca] = -1, addpath(lca, st[top]), st[top] = lca;
                else
                    addpath(lca, st[top--]);
            }
            head[curtpt] = -1, st[++top] = curtpt;
        }
        for (int i = 1; i < top; i++)
            addpath(st[i], st[i + 1]);
        dfs_1(1), dfs_2(1), dfs_3(1);
        for (int i = 0; i < m; i++)
            printf("%d ", anses[arrs[i]]);
        printf("\n");
        for (int i = 0; i < m; i++)
            vis[arrs[i]] = false;
    }
    return 0;
}

 

长沙游记

因为要来雅礼中学受虐,所以来到了长沙这座城市。这座城市经常被我的父辈们提起,并且把它与南昌比较:经济更发达、城市更大、轨道交通更现代化(甚至都有磁浮了),并且在这种情况下房价均价也要比南昌低不少。所以,我一直对长沙这个城市没有什么清晰的认识,也只是听我妈说在她仅为一天的长沙游中对长沙留下了“东西很好吃的印象”。

一个多小时的高铁并不是什么长途,沪昆线上飞驰的 CRH3\4 系经常进入我的视野。长沙南站看上去并没有南昌西站那么亮,但是确实热闹不少。地铁二号线和出租车把我送到了雅礼中学的门口。

雅礼中学的大门,说实话,低于我的预期。可能是历史过于悠久的原因,学校内的建筑的外观参差不齐,新中有旧,旧中有新。足球队员们在足球场上训练,也能见到不少没有穿雅礼校服的高中生。整个教学楼的设计是我比较中意的,感觉他们的教室应该很大(那也有可能是因为我所在的学校一个班 60 个人的原因)。

机房的氛围挺好,但是整洁并没有预期那么好,随处可见零食包装袋和电线,黑板上写着一些神仙东西,穿着雅礼校服和穿着便服的同学们在一个机房里写题。XG 就坐在我的对面。

之后的一周我基本上都在机房、酒店、便利店这三点来回走动。身体上比较愉悦,但是精神上因为某天晚上梦见某人,所以接下来的几天就变得非常的念旧,心情也非常的糟糕。大体上来讲,OI 水平还是提升了不少的,即使完全赶不上九江一中的神仙们。

周日的时候非常的闲,又碰上了正好也很闲的 XG 便是一天的颓废,去了长沙的一些地方,去吃了个晚饭,顺便看了看小米之家、骚尼便回了酒店。大体来讲,整个长沙市容不能说很好,跟红谷滩来讲差的很远,但是比那个什么垃圾老城区要好很多。要知道,一旦习惯于住在红谷滩的南昌人是永远不想回到桥对面的,因为你指不定就会碰上一大堆本该不会发生的破事。

长沙人的口音是真的很重,我在杭州几乎听不到什么口音,最多也就是腔调。但是长沙的口音是令人惊讶的普及:每个人说话都带有口音。一次我听一位巨佬讲概率期望,全程讲课都带有浓重的口音并且本地人毫无反应。我是第一次发现一个地方能把地方方言和普通话结合的如此地一体。一开始我在附中的机房就已经能听到 DOFY 和 QYQ 在用这种口音讲话,没想到他们在湖南集训时不小心学会的口音留到了现在。(当然,也有可能是因为他们本来就是湖南人。)

长沙估计也会常来,说不定就会发什么“又一篇长沙游记”这样标题的傻逼文章,之后有幸再来的时候再来补充吧。

P2577:「ZJOI2005」午餐题解

主要思路

容易想出状态\(dp[i][j][k]\)代表前\(i\)个人在一号队列排\(j\)分钟及在二号队排\(k\)分钟的最小时间。然后发现\(k\)这一维可以直接消去,因为有\(k=sum[i]-j\)。所以把线性依赖消去后,方程就非常小清新了:

\[dp[i][j] = min\{dp[i][j], max\{dp[i – 1][j – persons[i].a], j + persons[i].b\}\},当persons[i].a\leq j\]

\[dp[i][j] = min\{dp[i][j], max\{dp[i – 1][j], sum[i] – j + persons[i].b\}\}\]

代码

// P2577.cpp
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#define ll long long
using namespace std;
const int MX_N = 220;
struct person
{
    ll a, b;
    bool operator<(const person &ps) const
    {
        return b > ps.b;
    }
} persons[MX_N];
ll sum[MX_N], n, dp[MX_N][MX_N * MX_N];
int main()
{
    scanf("%lld", &n);
    for (int i = 1; i <= n; i++)
        scanf("%lld%lld", &persons[i].a, &persons[i].b);
    sort(persons + 1, persons + 1 + n);
    for (int i = 1; i <= n; i++)
        sum[i] = sum[i - 1] + persons[i].a;
    memset(dp, 0x3f, sizeof(dp));
    dp[0][0] = 0;
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= sum[i]; j++)
        {
            if (j >= persons[i].a)
                dp[i][j] = min(dp[i][j], max(dp[i - 1][j - persons[i].a], (ll)(j + persons[i].b)));
            dp[i][j] = min(dp[i][j], max(dp[i - 1][j], sum[i] - j + persons[i].b));
        }
    ll ans = 0x7fffffff;
    for (int i = 0; i <= sum[n]; i++)
        ans = min(ans, dp[n][i]);
    printf("%lld", ans);
    return 0;
}

用单调队列优化动态规划

Introduction

有的时候我们在日常写题时,会碰见这样的毒瘤 DP 题目,他们一般有以下特性:

  • 多维度动态规划
  • 循环层数很多,在极限数据下无法 AC。
  • 一般出现在区间 DP 中,一般暴力算法复杂度为\(O(n^3)\)。

这个时候我们可以使用单调队列来优化,时间复杂度可以进一步降至\(O(n^2)\)或者是\(O(nm)\)。如果有不会单调队列的同学呢可以去学习以下,顺便可以看看这篇文章:CH1201:最大子序和题解

Overall on this optimization technique

我们来看一道例题:Fence – POJ这道题是一道比较简单的经典 DP。设计状态\(dp[i][j]\)表示前\(i\)个人搞定了前\(j\)个单位的墙。写出方程式:

\[dp[i][j] = min_{S_i – l_i \leq k < S_i}\{dp[i-1][k]+profit_i*(j-k)\}\]

这样的话,整个算法的复杂度是\(O(n^3)\)的。我们能不能将其降为\(O(n^2)\)呢?我们可以把整个方程变个形,之后方程是这样的:

\[dp[i][j] = min_{S_i – l_i \leq k < S_i}\{dp[i-1][k]-profit_i*k\}+profit_i*j\]

我们发现,这样的话整个方程在\(i\)的循环之下,算是只跟变量\(k\)扯上关系了。我们会发现,只要有\(k_1<k_2\)且\(dp[i-1][k_1]-profit_i*k_1<dp[i-1][k_2]-profit_i*k_2\),那么就可以直接排除掉答案\(k_1\)。那么就可以使用单调队列来做到这一点。

下面是核心的 DP 代码:

int calc(int i, int k)
{
    return dp[i - 1][k] - wks[i].p * k;
}

// in ( int main() )
for (int i = 1; i <= k; i++)
{
    deque<int> q;
    for (int j = max(0, wks[i].s - wks[i].l); j <= wks[i].s - 1; j++)
    {
        while (!q.empty() && calc(i, q.back()) <= calc(i, j))
            q.pop_back();
        q.push_back(j);
    }
    for (int j = 1; j <= n; j++)
    {
        dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
        if (j >= wks[i].s)
        {
            while (!q.empty() && q.front() < j - wks[i].l)
                q.pop_front();
            if (!q.empty())
                dp[i][j] = max(dp[i][j], calc(i, q.front()) + wks[i].p * j);
        }
    }
}

In Conclusion

整个单调队列优化会为后面的斜率优化做基础(因为我学这个单调队列优化就是有一道题要求使用斜率优化),所以请务必搞懂这个操作。