乱七八糟的做题笔记

P1570 KC 喝咖啡

Source

给定 \(n\) 个元素,每个元素都有 \(v_i,c_i\) 两个值。选 \(m\) 个元素,使得 \(\dfrac{\sum v_i}{\sum c_i}\) 最大。

\(n\le 200\)。

Solution

01 分数规划板子。

令 \(\dfrac{\sum v_iw_i}{\sum c_iw_i}\ge mid(w_i\in\{0,1\})\) 然后移项。

\[\sum v_iw_i\ge mid\sum c_iw_i \]

\[\sum v_iw_i\ge \sum c_iw_i mid \]

\[\sum v_iw_i-\sum c_iw_imid\ge 0 \]

\[w_i\sum v_i-c_imid\ge 0 \]

对于 \(\forall mid\),计算 \(\forall (v_i-c_imid)\),取前 \(k\) 大判断是否大于等于 \(0\) 即可。

板子题。

Code
namespace solution
{
    constexpr int MAXN = 10000;
    constexpr int INF = 0x3f3f3f3f;
    int n,m;
    int v[N],c[N];
    bool cmp(int a,int b){return a>b;}
    bool check(int x)
    {
        int t[N];
        for(int i=1;i<=n;i++) t[i] = v[i]-c[i]*x;
        sort(t+1,t+n+1,cmp);
        int sum = 0; 
        for(int i=1;i<=m;i++) sum += t[i];
        return sum >= 0;
    }
    void solve()
    {
        cin>>n>>m;
        for(int i=1;i<=n;i++) {cin>>v[i];v[i]*=MAXN;}
        for(int i=1;i<=n;i++) {cin>>c[i];}
        int l = 1,r = 1e6;
        while(l <= r)
        {
            int mid = (l+r) >>1;
            if(check(mid)) l = mid + 1;
            else r = mid - 1;
        }
        printf("%.3lf\n",(double)(l*1.0/MAXN));
        return;
    }
}

P4281 [AHOI2008] 紧急集合 / 聚会

Source

给定一棵有根树,\(q\) 次询问,每次询问给定树上三个点 \(x,y,z\),求一个点 \(t\),使得 \(\text{dis}(x,t)+\text{dis}(y,t)+\text{dis}(z,t)\) 最小。\(\text{dis}(x,y)\) 表示树上点 \(x,y\) 的距离。

\(n\le 5\times 10^5\)。

Solution

大胆猜结论题。

首先考虑最简单的情况,若只有两个点,则 \(t\) 可在两点路径上的任意位置。

我们让 \(\text{dis}(x,t)+\text{dis}(y,t)+\text{dis}(z,t)\) 最小,就是让边尽可能不被重复走,每条边最多只能造成一次贡献。

到这里就可以画图找规律了。通过画图可看出,若将三点两两求 LCA,一定存在两对点的 LCA 相同。这个结论是对的。

我们最优点一定选在 LCA 上,若不选 LCA,会出现重复走的情况。

证明(chatgpt)

这个结论是正确的:在树上选择三个不同的点 (A)、(B)、(C),求它们两两之间的 LCA,总共得到三个 LCA。必定有两个 LCA 是相同的。下面给出证明。

证明思路:

我们假设树是以根为中心的(即根树结构),并使用树的父子关系来讨论三个点的 LCA 情况。

假设 ( \text{LCA}(A, B) = X ),( \text{LCA}(A, C) = Y ),( \text{LCA}(B, C) = Z )。需要证明其中至少有两个是相同的,即:( X = Y ) 或 ( X = Z ) 或 ( Y = Z )。

1. LCA 的基本性质:

  • 给定任意两个点 ( P ) 和 ( Q ),它们的 LCA 是它们在树上最低的共同祖先。
  • 如果点 ( P ) 和 ( Q ) 分别位于树的不同子树上,那么它们的 LCA 是位于这两个子树的分叉点的那个祖先。
  • LCA 总是离这两个点较近的公共祖先。

2. 三个点的关系分析:

考虑三个点 ( A )、( B )、( C ) 在树中的位置。它们可以分为以下三种情况:

情况 1:其中一个点是另两个点的祖先

假设 ( A ) 是 ( B ) 和 ( C ) 的祖先。根据 LCA 的定义:

  • ( \text{LCA}(A, B) = A )
  • ( \text{LCA}(A, C) = A )
  • ( \text{LCA}(B, C) ) 必定是 ( A ) 或 ( B ) 和 ( C ) 的某个共同祖先,但由于 ( A ) 是 ( B ) 和 ( C ) 的祖先,LCA(B, C) 必定是 ( A )。

因此,三个 LCA 全部相同,均为 ( A )。

情况 2:某两个点在同一个子树,第三个点在另一个子树

假设 ( A ) 和 ( B ) 位于同一个子树,而 ( C ) 位于另一个子树。根据 LCA 的定义:

  • ( \text{LCA}(A, B) ) 是 ( A ) 和 ( B ) 的某个共同祖先,记为 ( X )。
  • ( \text{LCA}(A, C) = \text{LCA}(B, C) ),它们都等于 ( A ) 和 ( C )、( B ) 和 ( C ) 的公共祖先,因为 ( A ) 和 ( B ) 在同一个子树,( C ) 位于另一个子树。此时 LCA(A, C) 和 LCA(B, C) 必定相同,记为 ( Y )。

此时 ( X ) 和 ( Y ) 可能不同,但 ( \text{LCA}(A, C) = \text{LCA}(B, C) ),即 ( Y )。

情况 3:三个点分别位于三个不同的子树

这种情况下,( \text{LCA}(A, B) )、( \text{LCA}(A, C) )、( \text{LCA}(B, C) ) 都会等于三个点所在子树的分叉点,也就是它们三个点的最低公共祖先。因此,三个 LCA 全部相同。

3. 结论:

通过分析以上三种情况,至少有两个 LCA 是相同的。因此,结论得证:三个点两两求 LCA,得到的三个 LCA 中,必定有两个是相同的。

选哪个 LCA 呢?

我们要选 和另外两对点对不相同的 LCA。两队点对的 LCA 相同了,选那个相同的 LCA 势必会出现重复走的情况。然后计算答案即可。求 LCA 可以倍增,可以树剖,也可以各种奇怪做法。

Code
namespace solution
{
    int n,m;
    vector <int> Edge[N];
    struct TCS
    {
        int dfn[N],siz[N],son[N],fa[N],tp[N],depth[N];
        int cnt = 0;
        void dfs1(int u,int fat)
        {
            fa[u] = fat;
            depth[u] = depth[fat] + 1;
            int maxn = 0,maxx = 0;
            siz[u] = 1;
            for(auto v:Edge[u])
            {
                if(v == fat) continue;
                dfs1(v,u);
                siz[u] += siz[v];
                if(siz[v] > maxn)
                {
                    maxn = siz[v];
                    maxx = v;
                }
            }
            son[u] = maxx;
        }
        void dfs2(int u,int topp)
        {
            tp[u] = topp;
            dfn[u] = ++cnt;
            if(!son[u]) return;
            dfs2(son[u],topp);
            for(auto v:Edge[u])
            {
                if(v == son[u] || v == fa[u]) continue;
                dfs2(v,v);
            }
        }
        int get_lca(int u,int v)
        {
            while(tp[u] != tp[v])
            {
                if(depth[tp[u]] > depth[tp[v]]) swap(u,v);
                v = fa[tp[v]];
            }
            if(depth[u] > depth[v]) swap(u,v);
            return u;
        }
    }tcs;
    void solve()
    {
        cin>>n>>m;
        for(int i=1;i<n;i++)
        {
            int u,v;
            cin>>u>>v;
            Edge[u].push_back(v),Edge[v].push_back(u);
        }
        tcs.dfs1(1,0);
        tcs.dfs2(1,0);
        while(m--)
        {
            int x,y,z;
            cin>>x>>y>>z;
            int lca1 = tcs.get_lca(x,y),lca2 = tcs.get_lca(x,z),lca3 = tcs.get_lca(y,z);
            int min_lca = min({tcs.depth[lca1],tcs.depth[lca2],tcs.depth[lca3]});
            int max_lca = max({tcs.depth[lca1],tcs.depth[lca2],tcs.depth[lca3]});
            int ans = tcs.depth[x]+tcs.depth[y]+tcs.depth[z]-max_lca-(min_lca)*2;
            if(tcs.depth[lca1] == tcs.depth[lca2]) cout<<lca3<<" ";
            else if(tcs.depth[lca1] == tcs.depth[lca3]) cout<<lca2<<" ";
            else cout<<lca1<<" ";
            cout<<ans<<endl;
        }
        return;
    }
}

P4231 三步必杀

有一个长度为 \(n\) 的数组 \(a\),初始均为 \(0\)。有 \(q\) 次操作,每次操作四个参数 \(l,r,s,e\) 表示将区间 \([l,r]\) 加上一个等差数列,满足首项为 \(s\),尾项为 \(e\)。求 \(q\) 次操作后 \(\max a_i\) 和 \(a\) 数组的异或和。

\(n\le 10^7\),limit:500ms

Solution

我们做过一个类似的题,叫做 无聊的数列。回顾一下这个题。

维护一个数列 \(a_i\),支持两种操作:

  • 1 l r K D:给出一个长度等于 \(r-l+1\) 的等差数列,首项为 \(K\),公差为 \(D\),并将它对应加到 \([l,r]\) 范围中的每一个数上。即:令 \(a_l=a_l+K,a_{l+1}=a_{l+1}+K+D\ldots a_r=a_r+K+(r-l) \times D\)。
  • 2 p:询问序列的第 \(p\) 个数的值 \(a_p\)。
    \(n,m\le 10^5\)。

你会发现上边这个题是 修改和查询混在一块的,但这并不妨碍我们差分。每次询问线段树区间查询即可。数据范围较小线段树稳过。

但现在 \(n\le 10^7\)。时限 500ms,线段树,树状数组都过不了。我们不得不考虑线性做法。

考虑对区间 \([3,6]\) 加上首项为 \(2\),公差为 \(3\),尾项为 \(14\) 的等差数列是什么行为。

num:1 2 3 4 5 6  7 8
a:  0 2 5 8 11 14 0 0
b:  0 2 3 3 3 3 -14 0

这样还是没法维护。诶这不就是给区间 \([2,5]\) 加上公差 \(3\) 嘛。我们对 \(b\) 进行差分,记作 \(c\)。

num:1 2 3 4 5 6  7 8 9
a:  0 2 5 8 11 14 0 0 0
b:  0 2 3 3 3 3 -14 0 0
c:  0 2 1 0 0 0 -17 14 0

你会发现 无论加的等差数列有多长,中间那一串 \(0\) 是不变的。每次操作我们只需修改 \(c\) 数组四处即可。

即对于每次操作,我们只需令 \(c_l+s,c_{l+1}+(d-s),c_{r+1}-(e+d),c_{r+2}+e\) 即可。

还原数组只需做两遍前缀和即可。这个技巧叫做 二阶差分。

Code
namespace solution
{
    int n,m;
    int a[N],b[N];
    void solve()
    {
        cin>>n>>m;
        while(m--)
        {
            int l,r,s,e;
            cin>>l>>r>>s>>e;
            int gc = (e-s)/(r-l);
            a[l] += s;
            a[l+1] += (gc-s);
            a[r+1] += (-e-gc);
            a[r+2] += e;
        }
        for(int i=1;i<=n;i++) a[i] += a[i-1];
        int maxn = 0;
        int ans = 0;
        int sum = 0;
        for(int i=1;i<=n;i++)
        {
            sum += a[i];
            ans ^= sum;
            maxn = max(maxn,sum);
        }
        cout<<ans<<" "<<maxn<<endl;
        return;
    }
}

P6902 [ICPC2014 WF] Surveillance

Source

给定一个长度为 \(n\) 的环,有 \(k\) 个区域被覆盖,求最小的环被完全覆盖的区域数量。

Solution

经典模型

一个经典问题:给定 \(k\) 条线段,用最少的线段覆盖长度为 \(n\) 的区间。

我们维护线段集合 \(S\),当前覆盖到的最右端点 \(x\)。每次从线段集合中选取左端点小于等于 \(x+1\) 右端点最大的线段去覆盖,设该线段有端点为 \(y\),令 \(x\leftarrow y\)。

重复上述操作直到区间全部被覆盖。

基础模型的简单拓展

给定 \(k\) 条线段,\(q\) 次询问,每次询问最少用多少条线段覆盖区间 \([l,r]\)。

一个显然的暴力做法是,对于每个询问 \([l,r]\),令 \(x=l-1\) 跑上述贪心。

下文定义一轮greedy select 为从线段集合中进行一次上述贪心选择。

显然,当起点固定后,进行 \(k\) 轮 greedy select 后 \(x\) 是固定的,毕竟贪心策略一定。

考虑倍增维护每个起点经过若干轮 greedy select 后的 \(x\)。

令 \(f_{i,j}\) 为起点为 \(i\),经过 \(2^j\) 轮 greedy select 后的 \(x+1\)。查询的时候倍增跳即可。

接下来回到本题。

我们断环为链。对于每条线段 \([l,r]\),若 \(l>r\),即当前线段是同时跨 \(1\) 和 \(n\) 的。将 \(r+n\)。这算是经典做法了。

我们要覆盖环,但不知道从哪开始。这很简单,对于每个点 \(i\in[1,n]\) 将其作为起点,倍增跳到 \(i+n\) 及以外即可。

至于 \(f\) 数组初始化,这么写就行。

typedef pair<int,int> PAIR;
PAIR p[N];
//读入和断环为链
sort(p+1,p+1+k);
int r = 0;
for(int i=1,j=1;i<=n*2;i++)
{
    while(j <= k && p[j].x <= i) 
    {
        r = max(r,p[j].y+1);
        j++; 
    }
    f[0][i] = r;
}

你会发现实际操作起来和先前对 greedy select 的描述不一样。

实际操作时,我们将区间按照左端点排序,对于每个 \(i\),只要其左端点小于等于 \(i\),就使用线段。并尝试更新 \(r\),直到线段左端点不符合条件退出,并更新 \(f\) 数组。

对线段按照左端点排序后,随着 \(i\) 的右移动,\(j\) 的移动是单调的。双指针扫一下就好。

参考代码
namespace solution
{
    #define x first
    #define y second
    typedef pair<int,int> PAIR;
    PAIR p[N];
    int f[50][N];
    int n,k;
    void solve()
    {
        cin>>n>>k;
        for(int i=1;i<=k;i++)
        {
            int l,r;
            cin>>l>>r;
            if(l > r) r += n;
            p[i] = {l,r};
        }
        sort(p+1,p+1+k);
        int r = 0;
        for(int i=1,j=1,maxn = 0;i<=n*2;i++)
        {
          // maxn = 0;
            while(j <= k && p[j].x <= i) 
            {
                r = max(r,p[j].y+1);
                j++; 
            }

            f[0][i] = r;
        }
        for(int i=1;i<=20;i++)
            for(int j=1;j<=(n*2);j++) f[i][j] = f[i-1][f[i-1][j]];
        int ans = INF;
        for(int i=1;i<=n;i++)
        {
            int x = i,sum = 0;
            for(int j=20;j>=0;j--)
            {
               if(f[j][x] < i+n) 
               {
                    x = f[j][x];
                    sum += (1<<j);
               }
            }
            x = f[0][x];
            sum ++;
            if(x >= i+n) ans = min(ans,sum);
        }
        if(ans == INF) cout<<"impossible"<<endl;
        else cout<<ans<<endl;
        return;
    }
}

P5021 [NOIP2018 提高组] 赛道修建

C 城将要举办一系列的赛车比赛。在比赛前,需要在城内修建 \(m\) 条赛道。

C 城一共有 \(n\) 个路口,这些路口编号为 \(1,2,…,n\),有 \(n-1\) 条适合于修建赛道的双向通行的道路,每条道路连接着两个路口。其中,第 \(i\) 条道路连接的两个路口编号为 \(a_i\) 和 \(b_i\),该道路的长度为 \(l_i\)。借助这 \(n-1\) 条道路,从任何一个路口出发都能到达其他所有的路口。

一条赛道是一组互不相同的道路 \(e_1,e_2,…,e_k\),满足可以从某个路口出发,依次经过 道路 \(e_1,e_2,…,e_k\)(每条道路经过一次,不允许调头)到达另一个路口。一条赛道的长度等于经过的各道路的长度之和。为保证安全,要求每条道路至多被一条赛道经过。

目前赛道修建的方案尚未确定。你的任务是设计一种赛道修建的方案,使得修建的 \(m\) 条赛道中长度最小的赛道长度最大(即 \(m\) 条赛道中最短赛道的长度尽可能大)

img

Solution

妙妙题。

题目保证了 有 \(n-1\) 条道路,且图连通,这是一棵树。

注意到 “求长度最小的赛道长度最大值”。这是 “最小值最大” 问题,我们可以二分,把最优化问题转换为判定性问题。

作为当年 noip T3 防 ak 题 ,肯定给了充足的部分分。表格如上。

首先考虑当 \(m=1\),只修建一条赛道,求树的直径即可。

当 \(a_i=1\)时,图为菊花图,由于 不可走回头路,路径最多有两条边。对于每条边,若 \(\ge mid\),令其单独为一条路径。将所有 \(< mid\) 的边存入集合 \(S\),对其升序排序,双指针匹配即可。

当 \(b_i=a_i+1\) 时,是一条链。模拟扫一遍即可。

做完上述部分分可获得 \(11\times 5=55\) 的好成绩!考场上能拿 55 pts 已经 win 了。

接下来考虑正解。

我们考虑树上的一条路径长什么样子。设路径两端点为 \((u,v)\),要么路径 \(u.v\) 是一条链,要么路径为 \(u\rightarrow \text{LCA}(u,v)\rightarrow v\)。在树上,对于任意节点 \(u\)(除根节点外),有且仅有一个父亲 \(fa_u\)。故考虑 \(u\) 对 \(fa_u\) 的贡献。

定义 \(f_v\) 为以 \(v\) 为链头,最大的未匹配上赛道的链长。\(fa_v=u\)。边 \((u,v)\) 的边权为 \(w\)。

  • 若 \(f_v+w \ge mid\),令答案 \(+1\)。直接匹配。

  • 反之,点 \(u\) 可能是赛道两端点的 \(\text{LCA}\)。将信息记录,准备后续匹配。

当 \(u\) 的所有儿子 \(v\) 都遍历完,回溯的时候,我们对先前记录的信息进行匹配。匹配的时候贪心的匹配最小的。枚举每个 \(f_v+w\),若无法匹配,尝试更新 \(f_u\)。反之,匹配上了不可更新 \(f_u\)。

对于每个 \(f_v+w\),如何找到最小的与其匹配的呢?用 mutiset 即可。自动排序。不能用 set 因为重复的元素会全部删掉。当然优先队列也行。

上述匹配过程是一个树形 dp 的过程。

代码很简单。

参考代码
namespace solution
{
    typedef pair<int,int> PAIR;
    int n,m;
    int p,res;
    vector <PAIR> Edge[N];
    int siz[N];
    int f[N];
    void dfs(int u,int fa)
    {
        #define x first
        #define y second
        set <int> s;
        for(auto t:Edge[u])
        {
            int v = t.x,w = t.y;
            if(v == fa) continue;
            dfs(v,u);
            if(f[v] + w >= p) res ++;
            else s.insert(f[v] + w);
        }
        while(!s.empty())
        {
            set <int>::iterator it = s.begin();
            s.erase(it);
            set <int>::iterator it1 = s.lower_bound(p-*it);
            if(it1 == s.end()) f[u] = max(f[u],*it);
            else {res ++;s.erase(it1);}
        }
    }
    bool check(int k)
    {
        p = k;
        res = 0;
        fill(f+1,f+n+1,0);
        dfs(1,0);
        return res >= m;
    }
    void solve()
    {
        cin>>n>>m;
        for(int i=1;i<n;i++)
        {
            int u,v,w;
            cin>>u>>v>>w;
            Edge[u].push_back(PAIR(v,w));
            Edge[v].push_back(PAIR(u,w));
        }
        int l=0,r = INF;
        int answer;
        while (l<=r)
        {
            int mid = (l+r) >> 1;
            if(check(mid)) {l = mid+1;answer = mid;}
            else r = mid - 1;
        }
        cout<<answer<<endl;
    }
}

P4211 [LNOI2014] LCA

Source

给出一个 \(n\) 个节点的有根树(编号为 \(0\) 到 \(n-1\),根节点为 \(0\))。
一个点的深度定义为这个节点到根的距离 \(+1\)。
设 \(dep[i]\) 表示点 \(i\) 的深度,\(\operatorname{LCA}(i, j)\) 表示 \(i\) 与 \(j\) 的最近公共祖先。
有 \(m\) 次询问,每次询问给出 \(l, r, z\),求 \(\sum_{i=l}^r dep[\operatorname{LCA}(i,z)]\) 。

\(1\le n\le 5\times 10^4\)。

Solution

每次询问求区间内的所有数和 \(z\) 的 \(\text{LCA}\) 的深度。\(u\) 的深度即为 \(u\) 到 \(root\) 路径上经过点的数量。以此类推,两点 \(\text{LCA}\) 的深度即为两点到 \(root\) 路径的公共部分。因此,对于 \(\forall i\in\{l,r\}\),将其到 \(root\) 路径上所有点权 \(+1\),最后求 \(root\) 到 \(z\) 路径权值即可。树链剖分 + 线段树即可解决问题。

但这样复杂度太高了。

注意到区间 \([1,r]\) 和区间 \([1,r+1]\) 有大量重复部分。若将每次询问独立,每次清空线段树再重新 modify,就很慢。不妨将询问离线,按照右端点排序(左端点都是 \(1\))。
区间 \([l,r]\) 对 \(z\) 的贡献可以拆成区间 \([1,r]\) 对 \(z\) 的贡献减去区间 \([1,l-1]\) 对 \(z\) 的贡献。将区间 \([1,l-1],[1,r]\) 扔到集合 \(S\) 中,对集合 \(S\) 按照右端点排序。

对于每个拆分后的区间,在上一个下标 \(pos\) 基础上进行 modify。同时进行查询,若当前区间是 \([1,l-1]\),该询问答案减去 query 值,否则加。在拆区间的同时记录一下即可。

不会在线做法。

参考代码
constexpr int N = 1000010;
constexpr int mod = 201314;
namespace solution
{
    int n,m;
    int ans[N];
    vector <int> Edge[N];
    struct Node
    {
        int pos,z,id,flg;
        bool operator<(const Node& a)const{
            return pos < a.pos;
        }
    };
    vector <Node> q;
    struct segtree
    {
        #define ls p<<1
        #define rs p<<1|1
        int tree[N],tag[N];
        void pushdown(int p,int len)
        {
            if(!tag[p]) return;
            tree[ls] += (len-len/2)*tag[p],tree[rs] += (len/2)*tag[p];
            tag[ls] += tag[p],tag[rs] += tag[p];
            tag[p] = 0;
        }
        void pushup(int p){tree[p] = tree[ls] + tree[rs];}
        void modify(int l,int r,int pl,int pr,int p,int d)
        {
            if(l > pr || r < pl) return;
            if(l >= pl && r <= pr) {tree[p] += (r-l+1)*d;tag[p] += d;return;}
            int mid = (l+r) >> 1;
            pushdown(p,r-l+1);
            modify(l,mid,pl,pr,ls,d);
            modify(mid+1,r,pl,pr,rs,d);
            pushup(p);
        }
        int query(int l,int r,int pl,int pr,int p)
        {
            if(l > pr || r < pl) return 0;
            if(l >= pl && r <= pr) return tree[p];
            int mid = (l+r) >> 1;
            pushdown(p,r-l+1);
            return query(l,mid,pl,pr,ls) + query(mid+1,r,pl,pr,rs);
        }
    }seg;
    struct TCS
    {
        int dfn[N],siz[N],depth[N],son[N],tp[N],fa[N];
        int cnt = 0;
        void dfs1(int u,int f)
        {
            depth[u] = depth[f] + 1;
            siz[u] = 1;
            fa[u] = f;
            int maxn = 0,maxx = 0;
            for(auto v:Edge[u])
            {
                if(v == f) continue;
                dfs1(v,u);
                siz[u] += siz[v];
                if(siz[v] > maxn)
                {
                    maxn = siz[v];
                    maxx = v;
                }
            }
            son[u] = maxx;
        }
        void dfs2(int u,int topp)
        {
            dfn[u] = ++cnt;
            tp[u] = topp;
            if(!son[u]) return;
            dfs2(son[u],topp);
            for(auto v:Edge[u])
            {
                if(v == fa[u] || v == son[u]) continue;
                dfs2(v,v);
            }
        }
        void modify(int x,int y,int d)
        {
            while(tp[x] != tp[y])
            {
                if(depth[tp[x]] > depth[tp[y]]) swap(x,y);
                seg.modify(1,n,dfn[tp[y]],dfn[y],1,d);
                y = fa[tp[y]];
            }
            if(depth[x] > depth[y]) swap(x,y);
            seg.modify(1,n,dfn[x],dfn[y],1,d);
            return;
        }
        int query(int x,int y)
        {
            int sum = 0;
            while(tp[x] != tp[y])
            {
                if(depth[tp[x]] > depth[tp[y]]) swap(x,y);
                sum += seg.query(1,n,dfn[tp[y]],dfn[y],1);
                y = fa[tp[y]];
            }
            if(depth[x] > depth[y]) swap(x,y);
            sum += seg.query(1,n,dfn[x],dfn[y],1);
            return sum;
        }
    }tcs;
    void solve()
    {
        cin>>n>>m;
        for(int i=2;i<=n;i++) 
        {
            int f;
            cin>>f;
            f ++;
            Edge[f].push_back(i);
            Edge[i].push_back(f);
        }
        tcs.dfs1(1,0);
        tcs.dfs2(1,0);
        for(int i=1;i<=m;i++)
        {
            int l,r,z;
            cin>>l>>r>>z;
            l ++;
            r ++;
            z ++;
            q.push_back({l-1,z,i,0});
            q.push_back({r,z,i,1});
        }
        sort(q.begin(),q.end());
        int pos = 1;
        for(auto t:q)
        {
            while(pos <= t.pos) tcs.modify(1,pos++,1);
            if(t.flg) ans[t.id] += tcs.query(1,t.z);
            else ans[t.id] -= tcs.query(1,t.z);
            ans[t.id] += mod;
            ans[t.id] %= mod;
        }
        for(int i=1;i<=m;i++) cout<<ans[i]<<endl;
        return;
    }
}

P1600 [NOIP2016 提高组] 天天爱跑步

Source

有一棵 \(n\) 个节点的树,\(m\) 个人分别从 \(s_i\) 走到 \(t_i\)。每个人同时从 \(0\) 时刻开始走。每个节点都有一个值 \(w_i\)。对于 \(\forall w_i\),求 \(w_i\) 时刻,有多少个人走到了 \(i\) 点。

\(n\le 299998\)。

Solution 毒瘤思维题。

复杂度至少是 \(O(n)\),毕竟有 \(n\) 个观察员。

我们不能模拟每个人跑步的过程,不妨从观察员的角度出发,考虑每个人(每条路径)能对观察员做出的贡献。

令 \(\text{LCA}(u,v)\) 表示 \(u,v\) 的 LCA。\(u\rightarrow \text{LCA}(u,v)\) 表示上行路径,\(\text{\text{LCA}}(u,v)\rightarrow v\) 表示下行路径。则该路径影响到的观察点 要么在上行路径上,要么在下行路径上。分类讨论如下。

  • 若观测点 \(p\) 在上行路径上,路径 \((u,v)\) 可被观测到当且仅当 \(\text{depth}_{s_i}-\text{depth}_p=w_i\)。移项可得 \(\text{depth}_p=\text{depth}_{s_i}+w_i\)。

  • 若观测点 \(p\) 在下行路径上,路径 \((u,v)\) 可被观测到当且仅当 \(len-(\text{depth}_{e_i}-\operatorname{depth_p})=w_i\)。(\(len\) 为路径长度,即 \(len=\text{depth}_{s_i}+\text{depth}_{e_i}-2\times \text{depth}_{\text{LCA}(s_i,e_i)}\))。移项可得 \(len-\text{depth}_{e_i}=w_p-\text{depth}_p\)。

因此,在上行途中,满足上述条件的点可做出贡献,下行同理。如此,我们将上行下行分开,重复贡献后面再说。需要注意,能对观察点 \(p\) 做出贡献的点(起点/终点)一定在 以 \(p\) 为根的子树上。(起点终点都不在以 \(p\) 为根的子树上,路径怎么可能经过 \(p\) 呢?)这个性质非常优美,它使得我们在递归回溯时可很方便统计贡献。

综上所述,路径 \((s_i,e_i)\) 对点 \(p\) 有贡献当且仅当 \(\text{depth}_p=\text{depth}_{s_i}+w_i\) 且 \(s_i\) 在点 \(p\) 为根的子树内,或 \(len-\text{depth}_{e_i}=w_p-\text{depth}_p\) 且 \(e_i\) 在以 \(p\) 为根的子树内。

考虑如何统计子树 \(p\) 的贡献。

这就用到了上述结论。dfs 时分别将 \(\text{depth}_{s_i}+w_i,len-\text{depth}_{e_i}\) 扔到桶里。回溯时查询即可。

但是这样有问题,对于同一深度的不同子树,查询时桶内元素会重复。删去肯定不现实,毕竟当深度减小时,桶里元素还会使用。不妨每次递归开始前记录桶里值,回溯时计算差值即可。

此外,因为我们都是简单路径,故当 \(p\) 的贡献算完后,以 \(p\) 为 LCA 的路径端点贡献都要删去。开个 vector 记录一下即可。

最后,考虑若 LCA 可被观测到,那上行/下行都能观测到,得去掉一个。

开桶的时候需要注意负数下标出现,整体位移即可。

本题思路难想,细节多,出在 noip 难度大,是个毒瘤题。

参考代码
namespace solution
{
    typedef tuple <int,int,int> TUPLE;
    struct Node
    {
        int u,v,w;
    };
    vector <int> Edge[N];
    int n,m;
    int b1[N],b2[N],w[N];
    int st[N];
    vector <int> ed[N];
    vector <Node> lst[N];
    int ans[N];
    struct LCA
    {
        int f[N][50];
        int depth[N];
        void dfs(int u,int fa)
        {
            f[u][0] = fa;
            depth[u] = depth[fa] + 1;
            for(int i=1;i<=30;i++) f[u][i] = f[f[u][i-1]][i-1];
            for(auto v:Edge[u])
            {
                if(v == fa) continue;
                dfs(v,u);
            } 
        }
        int get_lca(int x,int y)
        {
            if(depth[x] > depth[y]) swap(x,y);
            for(int i=30;i>=0;i--)
            {
                if(f[y][i] && depth[f[y][i]] > depth[x]) y = f[y][i];
            }
            if(depth[x] != depth[y]) y = f[y][0];
            if(x == y) return x;
            for(int i=30;i>=0;i--)
            {
                if(f[x][i] && f[y][i] && f[x][i] != f[y][i]) 
                {
                    x = f[x][i];
                    y = f[y][i];
                }
            }
            return f[x][0];
        }
    }lca;
    void dfs(int u,int fa)
    {
        int num1 = b1[lca.depth[u]+w[u]],num2 = b2[w[u]-lca.depth[u]+INF];

        for(auto v:Edge[u])
        {
            if(v == fa) continue;
            dfs(v,u);
        }
        b1[lca.depth[u]] += st[u];
        for(auto t:ed[u])
            b2[t-lca.depth[u]+INF] ++;
        ans[u] += b1[lca.depth[u]+w[u]]-num1+b2[w[u]-lca.depth[u]+INF]-num2;
        for(auto t:lst[u])
        {
            b1[lca.depth[t.u]] --;
            b2[t.w-lca.depth[t.v]+INF] --;
        }
    }
    void solve()
    {
        cin>>n>>m;
        for(int i=1;i<n;i++)
        {
            int u,v;
            cin>>u>>v;
            Edge[u].push_back(v);
            Edge[v].push_back(u);
        }
        lca.dfs(1,0);
        for(int i=1;i<=n;i++) cin>>w[i];
        for(int i=1;i<=m;i++)
        {
            int u,v;
            cin>>u>>v;
            int LCA = lca.get_lca(u,v);
            int dist = lca.depth[u]+lca.depth[v]-(2*lca.depth[LCA]);
            lst[LCA].push_back({u,v,dist});
            ans[LCA] -= (lca.depth[u] -lca.depth[LCA] == w[LCA]);
            st[u] ++;
            ed[v].push_back(dist);
        }
        dfs(1,0);
        for(int i=1;i<=n;i++) cout<<ans[i]<<" ";
        cout<<endl;
        return;
    }
}

[ABC355F] MST Query

Source

给你一棵 \(n\) 个点的带边权的树,有 \(q\) 次询问,每次询问加一条带边权的边,输出当前的最小生成树的边权和。

\(2\le n\le 2\times 10^5,1\le c_i,w_i\le 10\)。

Solution

乍看不会做。注意到 \(w_i\le 10\),这玩意有啥用呢?

设原树边为 \(W_i\),每次新增的边 \(w_i\) 只会对 \(W_j > w_i(j\in\{1,n\})\) 有贡献。

我们维护十个并查集,\(D_i(i\in[1,n])\) 维护边权 \(\le i\) 的点连通情况。对于每次加边操作,我们从小到大遍历并查集。若点 \((u,v)\) 在 \(D_i\) 下连通,则 \(i\) 为原 \(MST\) 上的最大边。比较其和新加边权。若新加边更优,则删去原边,加入新边。当然不需要断开并查集。算贡献即可。

最后对于新加边,更新并查集连通情况。(从 \(w_i\) 到 \(10\) 合并并查集。)

NOIp 2023 三值逻辑

Source

小 L 今天学习了 Kleene 三值逻辑。

在三值逻辑中,一个变量的值可能为:真(\(\mathit{True}\),简写作 \(\mathit{T}\))、假(\(\mathit{False}\),简写作 \(\mathit{F}\))或未确定(\(\mathit{Unknown}\),简写作 \(\mathit{U}\))。

在三值逻辑上也可以定义逻辑运算。由于小 L 学习进度很慢,只掌握了逻辑非运算 \(\lnot\),其运算法则为:

\[\lnot \mathit{T} = \mathit{F}, \lnot \mathit{F} = \mathit{T}, \lnot\mathit{U} = \mathit{U}. \]

现在小 L 有 \(n\) 个三值逻辑变量 \(x_1,\cdots, x_n\)。小 L 想进行一些有趣的尝试,于是他写下了 \(m\) 条语句。语句有以下三种类型,其中 \(\leftarrow\) 表示赋值:

  1. \(x_i \leftarrow v\),其中 \(v\) 为 \(\mathit{T}, \mathit{F}, \mathit{U}\) 的一种;
  2. \(x_i \leftarrow x_j\);
  3. \(x_i \leftarrow \lnot x_j\)。

一开始,小 L 会给这些变量赋初值,然后按顺序运行这 \(m\) 条语句。

小 L 希望执行了所有语句后,所有变量的最终值与初值都相等。在此前提下,小 L 希望初值中 \(\mathit{Unknown}\) 的变量尽可能少。

在本题中,你需要帮助小 L 找到 \(\mathit{Unknown}\) 变量个数最少的赋初值方案,使得执行了所有语句后所有变量的最终值和初始值相等。小 L 保证,至少对于本题的所有测试用例,这样的赋初值方案都必然是存在的。

对于所有测试数据,保证:

  • \(1 \le t \le 6\),\(1 \le n,m \le 10 ^ 5\);
  • 对于每个操作,\(v\) 为 TFU+- 中的某个字符,\(1 \le i,j \le n\)。
Solution

不难发现,所有变量最终要么是本身(取反),要么指向另一个变量(取反)。对于操作 2,3,满足传递性,考虑并查集。并查集上的连边 \(u\rightarrow v\) 表示 \(u\) 的权值取决于 \(v\)。依据题意模拟后我们可以确定一些点的权值。下面考虑未知点。

考虑什么情况下点 \(x\) 的权值必须为 U

  • 点 \(x\) 的 DSU 祖先为 U。

  • 点 \(x\) 的 DSU 祖先为 \(-x\),即若赋值为 T/F 矛盾。只有 U 取反等于 U。

那么何情况下会出现情况 \(2\) 呢?一条链上显然不会矛盾。考虑环。注意到 若从 \(\forall x\) 出发,取反奇数次后回到 \(x\) 的环,会出现 \(x=-x\)。若出现该情况,整个连通块都是 U。

因此,我们建立带权并查集,判定奇环将权值 \(\operatorname{XOR}\) 即可。

实现还是挺复杂的。

参考代码
constexpr int N = 5e5;
namespace solution
{
    typedef pair<int,int> PAIR;
    int fa[N];
    bool flg[N];
    int val[N];
    int T,sub;
    int n,m;
    int ans = 0;
    bool vis[N];
    int f[N];
    vector <PAIR> Edge[N];
    vector <int> edge[N];
    int sum = 0;
    int get_siz(int u=n+2)
    {
        vis[u] = 1;
        int siz = 1;
        for(auto t:Edge[u])
        {
            int v = t.first,w = t.second;
            if(vis[v])
            {
                if(f[v] != (f[u]^w)) sum = 0;
            }
            else 
            {
                f[v] = f[u]^w;
                siz += get_siz(v);
            }
        }
        return siz;
    }
    int get(int u=n+2)
    {
        int siz = 1;
        for(auto t:edge[u])
            siz += get(t);
        return siz;
    }
    void init(){for(int i=1;i<=n*2;i++) {fa[i] = i;flg[i] = 0;}}
    void memset1()
    {
        memset(val,0,sizeof(val));
        memset(vis,0,sizeof(vis));
        memset(Edge,0,sizeof(Edge));
        memset(edge,0,sizeof(edge));
        memset(f,0,sizeof(f));
        ans = 0;
        sum = 0;
    }
    void solve()
    {
        cin>>n>>m;
        memset1();
        int T = n+1,U = n+2;
        init();
        while(m--)
        {
            char op;
            int x,y;
            cin>>op;
            if(op == 'T' || op == 'F' || op == 'U')
            {
                cin>>x;
                if(op == 'T') {fa[x] = T;flg[x] = 0;}
                else if(op == 'F'){fa[x] = T;flg[x] = 1;}
                else {fa[x] = U;flg[x] = 0;}
            }
            else if(op == '+')
            {
                cin>>x>>y;
                fa[x] = fa[y];
                flg[x] = flg[y];
            }
            else if(op == '-')
            {
                cin>>x>>y;
                fa[x] = fa[y];
                flg[x] = (!flg[y]);
            }
        }
        for(int i=1;i<=n;i++)
        {
            Edge[fa[i]].push_back(PAIR(i,flg[i]));
            Edge[i].push_back(PAIR(fa[i],flg[i]));
        }
        for(int i=1;i<=n;i++)
        {
            if(!vis[i]) 
            {
                sum = 1;f[i] = 1;
                int t = get_siz(i);
                if(!sum) ans += t;
            }
        }
        for(int i=1;i<=n;i++) edge[fa[i]].push_back(i);
        ans += get()-1;
        cout<<ans<<endl;
        return;
    }
    void mian()
    {
        cin>>sub>>T;
        while(T--) {solve();}
        return;
    }
}

P8097 [USACO22JAN] Farm Updates G

Source

Farmer John 经营着总共 \(N\) 个农场(\(1\le N\le 10^5\)),编号为 \(1\ldots N\)。最初,这些农场之间没有道路连接,并且每个农场都在活跃地生产牛奶。

由于经济的动态性,Farmer John 需要根据 \(Q\) 次更新操作(\(0\le Q\le 2\cdot 10^5\))对他的农场进行改造。更新操作有三种可能的形式:

  • (D x) 停用一个活跃的农场 \(x\),使其不再生产牛奶。

  • (A x y) 在两个活跃的农场 \(x\) 和 \(y\) 之间添加一条道路。

  • (R e) 删除之前添加的第 \(e\) 条道路(\(e = 1\) 是添加的第一条道路)。

一个农场 \(x\) 如果正在活跃地生产牛奶,或者可以通过一系列道路到达另一个活跃的农场,则称之为一个「有关的」农场。对于每个农场 \(x\),计算最大的 \(i\)(\(0\le i\le Q\)),使得农场 \(x\) 在第 \(i\) 次更新后是有关的。

Solution

注意到操作 \(2\) 保证了 连接的一定是两个活跃的农场,而两个“活跃的农场”所在的联通快答案都是一样的,操作 \(2\) 只是将两个联通快合并,并不会影响答案。

因此,对答案有影响的只有操作 \(1\) 和 \(3\)。即我们需要维护单点修改,删边操作。

删边并不好处理,我们可以时光回溯,初始化所有的点都为 \(0\),操作 \(1\) 即将点置为 \(1\),操作 \(2\) 加边。

不只并查集,很多不好实现删边操作的 DS 都可时光回溯。时光回溯还可用于 每个点依赖于后面点,且正序不好实现的情况,例如某次 xyd 模拟赛 T4,缩点后倒着跑圆方树。

首先将所有操作离线,得到最终状态,即将最终状态下仍存在的边连接,点权为 \(1\) 的点答案设为 \(q\)(共 \(q\) 次操作)这很简单,对于每个权值为 \(1\) 的点 dfs 扫一遍即可。

只是单点,单边修改显然不行。我们应当考虑其对其余点的影响。具体的,将点 \(u\) 置为 \(1\) 后,我们以其为 root 进行 dfs 遍历。对于遍历到的点,若之前是 \(0\),就更新答案为 \(i-1\)(\(i\) 是操作回合编号)。

对于连接边 \((u,v)\),操作后分别从 \(u,v\) 开始进行 dfs 遍历,更改答案即可。

注意到每个点最多被遍历一次,复杂度是线性的。

参考代码
constexpr int N = 1000010;
namespace solution
{
	#define x first
	#define y second
	typedef pair<int,int> PAIR;
	vector <int> Edge[N];
	PAIR edge[N],q[N];
	bool active[N],edge_vis[N];
	int n,m;
	int ans[N];
	bool vis[N];
	int tot = 0;
	void dfs(int u,int val)
	{
		vis[u] = 1;
		if(!ans[u]) ans[u] = val;
		for(auto v:Edge[u])
		{
			if(vis[v]) continue;
			active[v] |= active[u];
			dfs(v,val);
		}
	}
	void solve()
	{
		cin>>n>>m;
		fill(active+1,active+n+1,1);
		for(int i=1;i<=m;i++)
		{
			char op;
			int x;
			cin>>op;
			if(op == 'A') 
			{
				int u,v;
				cin>>u>>v;
				edge[++tot] = PAIR(u,v);
				edge_vis[tot] = 1;
			}	
			else if(op == 'D')
			{
				cin>>x;
				active[x] = 0;
				q[i] = {1,x};
			}
			else 
			{
				cin>>x;
				q[i] = {2,x};
				edge_vis[x] = 0;
			}
		}  
		for(int i=1;i<=tot;i++)
		{
			int u = edge[i].x,v = edge[i].y;
			if(edge_vis[i]) 
			{
				Edge[u].push_back(v);
				Edge[v].push_back(u);
			}
		}
		for(int i=1;i<=n;i++)
			if(active[i]) dfs(i,m);
		for(int i=m;i>=1;i--)
		{
			if(q[i].x == 1)
			{
				active[q[i].y] = 1;
				dfs(q[i].y,i-1);
			}
			else if(q[i].x == 2)
			{
				int u = edge[q[i].y].x,v = edge[q[i].y].y;
				Edge[u].push_back(v);
				Edge[v].push_back(u);
				if(active[u]) dfs(u,i-1);
				if(active[v]) dfs(v,i-1); 
			}
		}
		for(int i=1;i<=n;i++) cout<<ans[i]<<endl;
		return;
	}
}

P10381 「HOI R1」杂赛选比

给定一个长度为 \(n\) 的数组 \(a\),初始只有 \(a_1\) 是已被解锁的。现在有一个整数 \(i\),初始值为 \(1\)。现在对这个数组进行一个游戏:

  • 如果 \(a_i\) 未被解锁,游戏结束。
  • 否则他可以将 \(a_{i+1\sim i+a_i}\) 设置成已被解锁的,或是获得 \(a_i\) 个金币(如果 \(a_i=0\) 则无法解锁任何元素),然后将 \(i\) 加 \(1\)。

请你求出游戏结束后你能获得的最大金币数量。

多测。\(n\le 10^5,T \le 5,a_i\le 10^5\)。

Solution

注意到对于每个点 \(i\),我们可以选择取该节点元素,或者开一个区间。如果我们倒着考虑,开一个区间 \([i,i+a_i]\) 等价于 在区间 \([i,i+a_i]\) 中选择一个点 \(j\),表示对于区间 \([i,j-1]\),我们都执行决策 \(1\),且点 \(j\) 执行决策 \(2\)。

这个东西是满足最优子结构的。可以转移。

形式化地,若设 \(f_i\) 表示对于区间 \([i,n]\) 的最佳决策,则

\[f_i=\max(a_i,\max\limits_{j=1}^{a_i}(f_{i+j}+\sum\limits_{j=1}^{a_i}a_{i+j})) \]

这个转移是平方级别的。

数据范围 \(10^5\),且状态已经设计到最简。考虑用 ds 优化。用 ds 来维护 \(\max\limits_{j=1}^{a_i}(f_{i+j}+\sum\limits_{j=1}^{a_i}a_{i+j})\)。

考虑扫描端点,即 \(i\rightarrow (i-1)\) 时产生的变化。这是 ds 优化 dp 的常见思考方式。

拆分贡献,对于 \(\forall i\),更新完 \(f_i\) 后单点加即可。对于 \(\sum\limits_{j=1}^{a_i} a_{i+j}\),它的意义是 从当前 \(i\) 到 \(i+a_i\) 的 \(a_i\) 和。显然当 \(i\rightarrow (i-1)\) 时,其值会 \(+a_i\)。因此,我们对区间 \([l,n]\) 区间加 \(a_i\) 即可。这可以用线段树维护。

时间复杂度 \(O(n\log n)\)。

参考代码
constexpr int N = 8000010;
constexpr int INF = 0x3f3f3f3f;
namespace solution
{
    int T;
    int n;
    int a[N];
    int f[N];
    struct segtree
    {
        #define ls p<<1
        #define rs p<<1|1
        int tree[N],mark[N];
        void pushdown(int p)
        {
            tree[ls] += mark[p],tree[rs] += mark[p];
            mark[ls] += mark[p],mark[rs] += mark[p];
            mark[p] = 0;
        }
        void pushup(int p){tree[p] = max(tree[ls],tree[rs]);}
        void modify(int l,int r,int pl,int pr,int p,int d)
        {
            if(l > pr || r < pl) return;
            if(l >= pl && r <= pr) 
            {
                tree[p] += d;
                mark[p] += d;
                return;
            }
            int mid = (l+r) >> 1;
            pushdown(p);
            modify(l,mid,pl,pr,ls,d);
            modify(mid+1,r,pl,pr,rs,d);
            pushup(p);
        }
        int query(int l,int r,int pl,int pr,int p)
        {
            if(l > pr || r < pl) return -INF;
            if(l >= pl && r <= pr) return tree[p];
            int mid = (l+r) >> 1;
            pushdown(p);
            return max(query(l,mid,pl,pr,ls),query(mid+1,r,pl,pr,rs));
        }
    }seg;
    void solve()
    {
        cin>>n;
        fill(f+1,f+n+1,0);
        memset(seg.tree,0,sizeof(seg.tree));
        memset(seg.mark,0,sizeof(seg.mark));
        for(int i=1;i<=n;i++) cin>>a[i];
        for(int i=n;i;i--)
        {
            f[i] = a[i];
            f[i] = max(f[i],seg.query(1,n,i+1,min(i+a[i],n),1));
            seg.modify(1,n,i+1,n,1,a[i]);
            seg.modify(1,n,i,i,1,f[i]);
        }
        cout<<f[1]<<endl;
    }
    void mian()
    {
        cin>>T;
        while(T--) 
            solve();
    }
}

P6446 [COCI2010-2011#1] TABOVI

Source

给定两个长度为 \(n\) 的数组 \(a,b\),每次操作选择一个区间 \([l,r]\),使 \(\forall a_i(a_i\in[l,r])\),\(a_i+1\) 或 \(a_i-1\)。求至少进行多少次操作,将 \(a\) 数组变为 \(b\) 数组。

要求 solve 复杂度线性。

Solution

刷初赛刷到的。

贪心地考虑,我们肯定希望 操作的区间尽可能连续。如果考虑多项不太好做,毕竟高度都不一样。不妨只考虑相邻两项。

定义 \(c_i=b_i-a_i\),即 \(a_i\rightarrow b_i\) 做出的变化。\(c_i\) 是矢量,\(c_i>0\) 表示需要减,反之为加。

我们画图举例。

img

(每个柱形表示 \(c_i(c_i>0)\))。

\(c_1\) 是第一个,它一定产生 \(c_1\) 的贡献。

\(c_2<c_1\),删 \(c_1\) 的时候可以顺便删了,无贡献。

\(c_3>c_2\),注意到在删 \(c_1\) 时可以同时删 \(c_3\) 当且仅当当前高度 \(=c_2\)。所以是不可传递的。我们只需考虑相邻两项即可。\(c_3\) 高度 \(\le c_2\) 的部分可以顺便删了。大于 \(c_2\) 的部分会产生 \(c_3-c_2\) 的贡献。

综上所述,当 \(c_i\ge 0,c_{i-1}\ge 0\) 时,产生 \(\max(0,c_{i}-c{i-1})\) 的贡献。为什么是 \(c_{i-1}\) 而不是 \(c_{i+1}\) 呢?我们需要考虑 \(c_1\)。

至此,\(c_i\le 0\) 且 \(c_{i-1} >0\) 的部分讨论完毕。

img

\(c_i\le 0\) 且 \(c_{i-1}\le 0\) 情况同理。

不难发现,产生 \(\max(0,c_{i-1}-c_{i})\) 的贡献。

img

接下来考虑 \(c_{i-1}<0,c_i>0\) 情况。

发现无论如何,\(c_{i-1}\) 不对 \(c_i\) 造成贡献,产生 \(c_i\) 的贡献。

类似的,\(c_i<0,c_{i-1}>0\) 时,产生 \(-c_i\) 的贡献。

参考代码
namespace solution
{
    int n;
    int a[N],b[N];
    int c[N];
    void solve()
    {
        cin>>n;
        for(int i=1;i<=n;i++) cin>>a[i];
        for(int i=1;i<=n;i++) cin>>b[i],c[i] = b[i] - a[i];
        int res = 0;
        for(int i=1;i<=n;i++)
        {
            if(c[i] >= 0 && c[i-1] >= 0) res += max(c[i]-c[i-1],0);
            else if(c[i] <= 0 && c[i-1] <= 0) res += max(0,c[i-1]-c[i]); 
            else if(c[i] >= 0 && c[i-1] <= 0) res += c[i];
            else 
                res -= c[i];
        }
        cout<<res<<endl;
    }
}

P5008 [yLOI2018] 锦鲤抄

Source

给你一张有向图,每个点有一个点权。任意时刻你可以任意选择一个有入度的点,获得它的点权并把它和它的出边从图上删去。最多能选择 \(k\) 个点,求最多能获得多少点权。

本题采用捆绑测试。

  • Subtask 1(30 points):\(n = 10\),\(m = 50\)。
  • Subtask 2(30 points):\(n = 100001\),\(m = 500001\)。保证给出的图是一个有向无环图。
  • Subtask 3(20 points):\(n = 100002\),\(m = 500002\)。保证给出的图中,没有入度的点有且仅有一个。
  • Subtask 4(17 points):\(n = 100003\),\(m = 500003\)。
  • Subtask 5(3 points):\(n = 500004\),\(m = 2000004\)。

对于全部的测试点,保证 \(1 \leq n \leq 5 \times 10^5 + 4\),\(1 \leq m \leq 2 \times 10^6 + 4\),\(0 \leq w_i \leq 10^3\),\(0 \leq k \leq n\)。

不保证给出的图没有自环。

Hint

  • 请注意数据读入对程序效率造成的影响。
  • \(n\) 的末位数字可以帮助你快速的判断测试点所在的子任务。
Solution

拓扑排序好题。考察拓扑排序思想。

注意到每个节点是否能取依赖于以前点取的状态,而复杂度要求在 \(n\log n\) 以内。并不太好处理。这种情况下,我们应该看特殊性质,看特殊性质是否具有提示作用。

subtask 1 是暴力。每一个点都可以取或者不取,算出情况后再判定是否合法即可。时间复杂度 \(O(2^n)\) 可以通过 subtask 1。

sub2 保证图是个 DAG。注意到在 DAG 上,对于任意有向边 \(u\rightarrow v\),删除 \(v\) 后对 \(u\) 无影响。因此,除了本来没有入度的点,其余的点我们都可以拿到(按照“相对拓扑序”的逆序取即可)因此,把所有本来有入度的点按照权值排序,取前 \(k\) 大即可。时间复杂度 \(O(n\log n)\)。

回顾一下拓扑排序:每次找到图上入度为 \(0\) 的节点入队,然后对于每个入度为 \(0\) 的节点 \(u\),删去它的出度(即对于有向边 \(u\rightarrow v\),\(v\) 的入度 \(-1\))。若 \(v\) 入度为 \(0\) 就入队。

而对于本题,一个节点能取当且仅当其有入度。若建反图,一个点能取当且仅当其有出度。我们对反图进行拓扑排序,按照反图拓扑序进行取点。除了本来就没有出度的点。其余点都可取到。

到这就有 \(60\) pts 了。有点牛。

DAG 我们可以很容易解决,对于非 DAG 图呢?我们想到了 SCC 缩点。但一个 SCC 内并不一定所有点都可取到。

Claim:若一个 SCC 是孤立的(即若将 SCC 看作一个点,没有入度),一定有一个点取不到。反之,SCC 内的点都可取到。需要特别注意的是,若 SCC 内某个点出现自环,也是可以的。

Proof:下文称“一个 SCC 有入度” 当且仅当有边引入 SCC 及 SCC 内存在一点有自环时。

若一个 SCC 有入度,设有 非 SCC 点入度 点为 \(u\),令 \(u\) 最后取即可。

反之,无论如何顺序取,最后总有一个点无入度。无法取到。

若在反图上考虑,一个“没有出度的 SCC”删到最后总有一个点是没有出度的。反之成立。

因此,对于所有 SCC,首先判断是否可全取到,若无法全取到,贪心地放弃 SCC 内点权最小的。这样一定更优不劣。

参考代码
#define int long long
using namespace std;
constexpr int N = 1000010;
constexpr int INF = 0x3f3f3f3f;
namespace solution
{
    int n,m,k;
    vector <int> Edge[N],edge[N];
    struct Node
    {
        int val;
        bool vis = 0;
        bool operator<(const Node& a)const{
            return a.val < val;
        }
    }qwq[N];
    int in[N];
    int scc_minn[N];
    struct SCC
    {
        int dfn[N],low[N];
        int cnt = 0,fa_cnt = 0,fa[N];
        stack <int> s;
        bool in_stack[N],vis[N];
        void tarjan(int u)
        {
            s.push(u);
            in_stack[u] = 1;
            vis[u] = 1;
            dfn[u] = low[u] = ++cnt;
            for(auto v:Edge[u])
            {
                if(u == v) continue;
                if(!dfn[v])
                {
                    tarjan(v);
                    low[u] = min(low[u],low[v]);
                }
                else if(in_stack[v])
                    low[u] = min(low[u],dfn[v]);
            }
            if(dfn[u] == low[u])
            {
                fa_cnt ++;
                int minn = INF,minx = -1;
                int siz = 0;
                while(s.top() != u)
                {
                    siz ++;
                    int p = s.top();
                    s.pop();
                    fa[p] = fa_cnt;
                    if(qwq[p].val < minn) 
                    {
                        minn = qwq[p].val;
                        minx = p;
                    }
                    in_stack[p] = 0;
                }
                s.pop();
                siz ++;
                if(qwq[u].val < minn){minn = qwq[u].val;minx = u;}
                fa[u] = fa_cnt;
                in_stack[u] = 0;
                  if(siz > 1)
                    scc_minn[fa_cnt] = minx;
            }       
        }
    }scc;
    void solve()
    {
        cin>>n>>m>>k;
        for(int i=1;i<=n;i++) cin>>qwq[i].val,qwq[i].vis = 0;
        for(int i=1;i<=m;i++)
        {
            int u,v;
            cin>>u>>v;
            Edge[u].push_back(v);
            qwq[v].vis = 1;
        } 
        for(int i=1;i<=n;i++) if(!scc.dfn[i]) scc.tarjan(i);
        for(int u=1;u<=n;u++)
        {
            for(auto v:Edge[u])
            {
                if(scc.fa[u] != scc.fa[v] || u == v) in[scc.fa[v]] =1; 
            } 
        }
        for(int i=1;i<=scc.cnt;i++) 
            if(!in[i]) {qwq[scc_minn[i]].vis = 0;}
        sort(qwq+1,qwq+n+1);
        int sum = 0;
        for(int i=1;i<=n;i++) 
            if(qwq[i].vis && k){sum += qwq[i].val;k--;if(!k) break;}
        cout<<sum<<endl;
        return;
    }
}

quq

口胡的题。

给定一个长度为 \(n\) 的数组 \(a\),求一个最长子序列 \(b\),满足 \(\forall b_i\space \& b_{i+1} \ne 0\)。

要求复杂度线性。

Solution

既然是 \(\operatorname{and}\) 操作,我们一位一位考虑。令 \(f_i\) 表示第 \(i\) 位为 \(1\) 的子序列长度。

对于每个数 \(x\) 进行二进制拆分。

  • 若该数的第 \(i\) 位为 \(1\),显然可以拼到 \(f_i\)。\(f_i+1\) 是一种合法的答案。统计 \(\max(f_i+1)\)

  • 若该数的第 \(i\) 位为 \(0\),由于 \(1\& 0=0,0\& 0=0\),不可能产生贡献。

注意到上面我们 并没有 当第 \(i\) 位为 \(1\) 时,令 \(f_i+1\)。而是统计 \(\max(f_i+1)\)。既然对于所有 \(f_j\)(满足 \(x\) 的第 \(j\) 位为 \(1\))都可将 \(x\) 拼到后面。且我们只关心相邻两数的 \(\operatorname{and}\),\(x\) 前面拼了什么已经无关紧要了,反正都是合法的。我们将每个 \(f_j\)(满足 \(x\) 的第 \(j\) 位为 \(1\))的值都更新为 \(\max(f_i+1)\) 即可。

上述做法的合法性在于,对于子序列 \(b\),我们只关心它当前的最后一位。故前面的取当前最大的拼上即可。

参考代码
namespace solution
{
    constexpr int N = 1000010;
    int n;
    int f[N];
    void solve()
    {
        cin>>n;
        for(int i=1;i<=n;i++)
        {
            int x;
            cin>>x;
            int maxn = 0;
            for(int i=0;i<30;i++)
                if(x&(1<<i)) maxn = max(maxn,f[i]+1);
            for(int i=0;i<30;i++) 
                if(x&(1<<i)) f[i] = maxn;
        }
        int ans = 0;
        for(int i=0;i<30;i++) ans = max(ans,f[i]);
        cout<<ans<<endl;
        return;
    }
}

P2680 [NOIP2015 提高组] 运输计划

Source

在一棵树上,每条边都有权值 \(w_i\)。有 \(m\) 条路径,每条路径描述为 \((u\rightarrow v)\)。其代价为经过边的权值和。你可以使一条边的权值变为 \(0\),求所有路径的权值的最大值最小。

\(n\le 3\times 10^5\)。

Solution

看到 “最大值最小” 问题,想到二分答案。将最值问题转换为判定性问题。考虑如何 check。

check 前,需预处理每条路径权值 \(w\)。这是必要的,可以用树上差分或者树剖解决。

对于每次 check,本来路径权值就小于等于 \(mid\) 的显然不用管。我们记录所有权值大于 \(mid\) 的边,记为集合 \(S\)。修改的边一定属于路径集合 \(S\) 的交集,(称其为 “可以改的边”)。否则必不满足题意。求路径交集用树上差分即可。

接下来,我们枚举每一条 可以改的边。check 本来路径权值最大的路径减去改边权是否符合题意,如果改边符合,则其他边必定符合。

参考代码
constexpr int N = 1000010;
constexpr int INF = 1e11;
namespace solution
{
    typedef pair<int,int> PAIR;
    vector <PAIR> Edge[N];
    int L;
    struct PATH
    {
        int u,v,val;
        bool operator<(const PATH& a)const{
            return a.val < val;
        }
    };
    vector <PATH> path;
    int n,m;
    int a[N];
    int maxnpath = 0;
    struct segtree
    {
        #define ls p<<1
        #define rs p<<1|1
        int tree[N];
        void pushup(int p){tree[p] = tree[ls] + tree[rs];}
        void build(int l=1,int r=n,int p=1)
        {
            if(l == r) {tree[p] = a[l];return;}
            int mid = (l+r) >> 1;
            build(l,mid,ls);
            build(mid+1,r,rs);
            pushup(p);
        }
        int query(int l,int r,int pl,int pr,int p)
        {
            if(l > pr || r < pl) return 0;
            if(l >= pl && r <= pr) return tree[p];
            int mid = (l+r) >> 1;
            return query(l,mid,pl,pr,ls) + query(mid+1,r,pl,pr,rs);
        }
    }seg;
    int dnf[N];
    int tot = 0;
    struct TCS
    {
        int L;
        #define x first
        #define y second
        int dfn[N],siz[N],tp[N],fa[N],depth[N],son[N],val[N];
        int cnt = 0;
        void dfs1(int u=1,int fat=0)
        {
            dnf[++tot] = u;
            depth[u] = depth[fat] + 1;
            siz[u] = 1;
            fa[u] = fat;
            int maxn = 0,maxx = 0;
            for(auto t:Edge[u])
            {
                int v = t.x,w = t.y;
                if(v == fat) continue;
                val[v] = w;
                dfs1(v,u);
                siz[u] += siz[v];
                if(siz[v] > maxn) {maxn = siz[v];maxx = v;}
            }
            son[u] = maxx;
        }
        void dfs2(int u=1,int topp=0)
        {
            dfn[u] = ++cnt;
            a[cnt] = val[u];
            tp[u] = topp;
            if(!son[u]) return;
            dfs2(son[u],topp);
            for(auto t:Edge[u])
            {
                int v = t.x;
                if(v == fa[u] || v == son[u]) continue;
                dfs2(v,v);
            }
        }
        PAIR query(int a,int b)
        {
            int sum = 0;
            while(tp[a] != tp[b])
            {
                if(depth[tp[a]] > depth[tp[b]]) swap(a,b);
                sum += seg.query(1,n,dfn[tp[b]],dfn[b],1);
                b = fa[tp[b]];
            }
            if(depth[a] > depth[b]) swap(a,b);
            sum += seg.query(1,n,dfn[a]+1,dfn[b],1);
            return PAIR(sum,a);
        }
    }tcs;
    struct BS
    {
        bool check(int k)
        {
            int res[N];
            int num = 0;
            fill(res+1,res+n+1,0);
            for(auto t:path)
            {
                int u = t.u,v = t.v,w = t.val;
                if(w <= k) continue;
                num ++;
                res[u] ++;
                res[v] ++;
                int LCA = tcs.query(u,v).second;
                res[LCA] -= 2;
            }
            for(int i=n;i>=1;i--)
            {
                res[tcs.fa[dnf[i]]] += res[dnf[i]];
                if(res[dnf[i]] == num && tcs.val[dnf[i]] >= maxnpath - k) return 1;
            }
            return 0;
        }
        void Binary_Search()
        {
            int l = 0,r = INF;
            while(l < r)
            {
                int mid = (l+r) >> 1;
                if(check(mid)) r = mid;
                else l = mid + 1;
            }
            cout<<l<<endl;
            return;
        }
    }bs;
    void solve()
    {
        cin>>n>>m;
        for(int i=1;i<n;i++) 
        {
            int u,v,w;
            cin>>u>>v>>w;
            Edge[u].push_back(PAIR(v,w));
            Edge[v].push_back(PAIR(u,w));
            L = max(L,w);
        }
        tcs.dfs1();
        tcs.dfs2();
        seg.build();
        for(int i=1;i<=m;i++)
        {
            int u,v;
            cin>>u>>v;
            int val = tcs.query(u,v).first;
            path.push_back({u,v,val});
            maxnpath = max(maxnpath,val);
        }
     //   sort(path.begin(),path.end());
        bs.Binary_Search();   
    }
}

ABC372E K-th Largest Connected Components

Source

有一个无向图,图中有 \(N\) 个顶点和 \(0\) 条边。顶点编号为 \(1\) 至 \(N\) 。

我们会给你 \(Q\) 个查询,让你按顺序处理。每个查询属于以下两种类型之一:

  • 类型 \(1\) :以 1 u v 格式给出。在顶点 \(u\) 和 \(v\) 之间添加一条边。

  • 类型 \(2\) :格式为 2 v k.打印与顶点 \(v\) 连接的顶点中 \(k\) /th 最大的顶点编号。如果连接到顶点 \(v\) 的顶点少于 \(k\) ,则打印 -1。

  • \(1 \leq N, Q \leq 2 \times 10^5\)

  • In a Type \(1\) query, \(1 \leq u < v \leq N\).

  • In a Type \(2\) query, \(1 \leq v \leq N\), \(1 \leq k \leq 10\).

  • All input values are integers.

Solution

简单题。

注意到本题背景是无向图。而无向图中,一个连通块内,答案必定是一样的。因此答案满足传递性。考虑并查集。

然后我们又发现 \(k\le 10\),开个二维数组就能存下答案。

合并的时候,两个节点 root 的答案一定是有序的,这很显然,初始化每个节点的答案只有其本身。类似分治的思想,每次合并我们都在执行一个子任务。

既然两个节点的答案都是有序的,二路归并即可。每次归并复杂度是线性的。由于询问的 \(k\) 最大就是十,归并完的结果超过十个扔掉就好了。

这样复杂度就是对的。

参考代码
namespace solution
{
    constexpr int N = 1000010;
    int n,q;
    int num[N];
    int fa[N];
    int k;
    int ans[N][100];
    int a[N];
    void Merge(int u,int v)
    {
        int cnt = 1;
        int len = num[u] + num[v];
        int p1 = 1,p2 = 1;
        while(p1 <= num[u]+1 && p2 <= num[v]+1)
        {
            int t = 0;
            if(p1 == num[u]+1) t = ans[v][p2++];
            else if(p2 == num[v]+1) t = ans[u][p1++];
            else if(ans[u][p1] > ans[v][p2]) t = ans[u][p1++];
            else if(ans[u][p1] == ans[v][p2]) 
            {
                t = ans[u][p1++];
                len --;
                p2 ++;
            }
            else t = ans[v][p2++];
            a[cnt++] = t;
        }
        for(int i=1;i<=min(len,10);i++) ans[u][i] = a[i];
        num[u] = min(len,10);
        return;
    }
    struct DSU
    {
        void init(){for(int i=1;i<=n;i++) fa[i] = i;}
        int find(int x)
        {
            if(fa[x] == x) return x;
            fa[x] = find(fa[x]);
            return fa[x];
        }
        void merge(int u,int v)
        {
            int i = find(u),j = find(v);
            Merge(i,j);
            fa[j] = fa[i];
            
        }
    }dsu;
    void solve()
    {
        cin>>n>>q;
         dsu.init();
         for(int i=1;i<=n;i++) {num[i] = 1,ans[i][1] = i;}
         while(q--)
         {
            int op,u,v;
            cin>>op;
            if(op == 1)
            {
                cin>>u>>v;
                dsu.merge(u,v);
            }
            else
            {
                cin>>u>>k;
                if(!ans[dsu.find(u)][k]) cout<<"-1"<<endl;
                else cout<<(ans[dsu.find(u)][k])<<endl;
            }
         }
    }
}

CF2000E Photoshoot for Gorillas

共有 \(w\) 个元素,每个元素有一个价值 \(a_i\),有一个 \(n\times m\) 的网格,你需要将这 \(w\) 个元素放到格子上,每个格子只能放一个元素。

放置完毕后,产生的贡献为 格子上所有大小为 \(k\times k\) 的子矩阵所覆盖的元素价值和(重复的算多次),最大化价值。

\(n\times m\le 2\times 10^5,\sum w \le 2\times 10^5\)。

Solution

注意到重复的算多次,我们一定希望价值大的尽可能多的被统计。

证明:考虑排序不等式,形如 \(\sum\limits_{i=1}^n a_i\times b_i\) 的式子,最大值存在当且仅当 \(a,b\) 降序排序。本题中,我们令序列 \(b\) 为每个网格被重复统计的次数。

如何处理每个网格被覆盖次数呢?显然用二维差分。差分后生成大小为 \(n\times m\) 的序列 \(b\),对序列 \(a,b\) 进行降序排序,然后算贡献即可。

复习二维差分。

参考代码
namespace solution
{
    int T;
    int n,m,k;
    int w;
    int a[N];
    int res[N];
    int cnt = 0;
    bool cmp(int a,int b){return a > b;}
    void solve()
    {
        cin>>n>>m>>k;
        int c[n+10][m+10];
        int sum[n+10][m+10];
        memset(c,0,sizeof(c));
        memset(sum,0,sizeof(sum));
        memset(res,0,sizeof(res));
        cnt = 0;
        cin>>w;
        for(int i=1;i<=w;i++) cin>>a[i];
        for(int i=1;i<=(n-k+1);i++)
        {
            for(int j=1;j<=(m-k+1);j++)
            {
                int sx = min(n,i+k-1),sy = min(m,j+k-1);
                c[i][j] ++;
                c[sx+1][sy+1] ++;
                c[i][sy+1] --;
                c[sx+1][j] --;
            }
        }
        for(int i=1;i<=n;i++)
        {
            for(int j=1;j<=m;j++) sum[i][j] = sum[i-1][j] + sum[i][j-1] - sum[i-1][j-1] + c[i][j];
        }
        for(int i=1;i<=n;i++)
            for(int j=1;j<=m;j++) res[++cnt] = sum[i][j];
        sort(a+1,a+w+1,cmp);
        sort(res+1,res+cnt+1,cmp);
        int ans = 0;
        for(int i=1;i<=min(w,cnt);i++) ans += a[i]*res[i];
        cout<<ans<<endl;
        return;
    }
    void mian()
    {
        cin>>T;
        while(T--)
        {
         //   memset0();
            solve();
        }
    }
}

P1823 [COI2007] Patrik 音乐会的等待

有一个长度为 \(n\) 的序列 \(a\),对于 \(\forall(i,j)\),若满足 \(\forall k\in (i,j),a_k<a_i,a_k<a_j\),称 \((i,j)\) 是“可见的”,求有多少个“可见的”点对。

\(n\le 5\times 10^5\)。

Solution

\(n\le 5\times 10^5\),复杂度要么线性要么带个 \(\log\)。

题目要求有多少个点对 \((i,j)\) 满足 \((i,j)\) 内没有元素大于等于 \(a_i,a_j\)。

注意到一个元素可以向左查找,也可以向右查找。但两边都查找会造成重复。因此,我们只考虑向左查找。即枚举 \(i\in[1,n]\) 作为点对右边的值时,能看到多少个 \(j\in[1,i-1]\)。这是处理点对问题的常见思路。

考虑 \(i\) 左侧有个元素 \(j\),满足 \(a_j\ge a_i\)。则 \(j\) 左侧的元素 \(i\) 都看不到了。进一步地,\(i\) 能看见 从 \(i+1\) 到第一个比 \(a_i\) 大的位置前(即左闭右开区间)。求解一个数左边第一个比其大的位置,用单调栈维护。我们还可以这样考虑,一个原本在栈中的数 \(a_x\) 能被 \(a_i\) 弹栈,意味着 \(a_x < a_i\),由于栈是单调的,\(a_x\) 到 \(a_i\) 已不存在比 \(a_i\) 大的数。否则无法弹出 \(a_x\)。第二种考虑方式补充了单调栈还可处理区间 \((l,r)\) 内没有比 \(a_l\) 更大的值。

总结一下,本题统计的点对 \((l,r)\) 需满足两个性质。

  • \(\forall a_i(i\in(l,r)) < a_l\)

  • \(\forall a_i(i\in(l,r)) < a_r\)

满足性质 2 需要统计 \(r\) 左边第一个比其大的位置。这通常用单调栈维护。而单调栈又保证了 \(a_i(i\in(l,r))<a_l\)。

再考虑元素相同的情况。

根据题意,若 \(a_i=a_j\),他们两个是不会互相遮挡的。因此,\(a_i\) 向左能看到的,\(a_j\) 也是可以的。重复元素不能随便删。非重复元素可以直接删掉的原因在于,若 \(a_i>a_k\),后面的元素肯定看不到 \(a_k\)。若 \(a_i=a_k\),后面的元素还可能看到 \(a_k\),故不能随便删。

这也比较好处理,记录一个点向左能看到几个重复元素,每次删除到这个元素时合并即可。

Solution
constexpr int N = 10000010;
namespace solution
{
    int n;
    int a[N];
    int r[N];
    int num[N];
    stack <int> s;
    void solve()
    {
        int res = 0;
        cin>>n;
        for(int i=1;i<=n;i++)
            cin>>a[i],num[i] = 1;
        s.push(1);
        for(int i=2;i<=n;i++)
        {
            while(s.size() && a[s.top()] <= a[i]) 
            {
                res +=num[s.top()];
                if(a[s.top()] == a[i])
                {
                    num[i] +=num[s.top()];
                }
                s.pop();
            }
            if(!s.empty()) res ++; // 相邻元素是可以无条件看到的
            s.push(i);
        }
        cout<<res<<endl;
    }
}

[ABC372D] Buildings

Source

给定一个长度为 \(n\) 的序列,求

\[\sum\limits_{j=i+1}^n[\max\limits_{k=i+1}^j h_k=h_i] \]

即对于 \(i\) 而言,求有多少个 \(j\),使得 \(h_j\) 是 \(h_{i+1}\)到 \(h_i\) 中最大的。

\(1\le n\le 2\times 10^5\)。

Solution

题目要求对于 \(\forall i\in[1,n]\),求有多少个 \(j\in[i,n]\),满足 \(\max\limits_{k=i}^j a_k=a_j\)。

如果我们将问题转化为,对于 \(\forall i\in[1,n]\),求有多少个 \(j\in[1,i)\),满足 \(\max\limits_{k=j}^i a_k=a_i\)。我们可以用单调栈轻松解决问题。只需找到每个 \(a_i\) 左边第一个大于它的数即可。原因显然。

上述操作我们枚举的右端点。计算的是对于每个数 \(a_i\),其左边第一个大于它的数。即将 \(a_i\) 作为右端点,有多少个左端点 \(j\),满足 \(\max\limits_{k=j}^i a_k=a_i\)。

回到原题,我们需求每个 \(i\) 向右有多少个 \(j\),满足 \(\max\limits_{k=i+1}^j a_k=a_j\)。一个很自然的思路是枚举左端点,但是并不好做。不妨枚举右端点,考虑其能对多少个左端点产生贡献。显然,对于任意右端点 \(i\),其能对向左连续比其小的值产生 \(1\) 的贡献,又回到了上面的问题,差分计算即可。

这种转换思想值得借鉴。

参考代码
constexpr int N = 1000010;
namespace solution
{
    int n;
    int a[N];
    stack <int> s;
    int res[N];
    int c[N];
    void solve()
    {
        cin>>n;
        for(int i=1;i<=n;i++) cin>>a[i];
        s.push(1);
        for(int i=2;i<=n;i++)
        {
            int lst = 0;
            while(s.size() && a[s.top()] <= a[i])
            {
                lst = s.top();
                s.pop();
            }
            if(!s.empty())
                res[i] = s.top();
            s.push(i);
        }
        for(int i=1;i<=n;i++) c[res[i]] ++,c[i] --;
        for(int i=1;i<=n;i++) c[i] += c[i-1];
        for(int i=1;i<=n;i++) cout<<c[i]<<" ";
    }
}

专题:模拟/暴力/代码能力专项训练

这里主要记录一下模拟犯的错误,常出错的地方。

  • 一个形如
1 2 3
4 5 6
7 8 9

的 \(n\times n\) 矩阵,数 \(x\) 所在行为 \(\lceil\dfrac{x}{n}\rceil\),若 \(x\) 是 \(n\) 的倍数,就在第 \(n\) 列,否则在 \(x\bmod n\) 列。(注意不是所谓 \(\lfloor\dfrac{x}{n}\rfloor\times n\) 列,这个式子成立当且仅当 \(x\bmod n\ne 0\),会出现问题。)

模拟的时候,若一步操作有两个操作,注意两个操作之间的顺序。最后输出答案前检查一下答案是否合法。当心出问题。

一个长得很有规律的矩阵,求一些东西或进行一些操作,往往不需要把整个矩阵存下来。我们可以尝试离线做,只记几个关键点,即所求的点,其他的点不必在意。

事实上,对于矩阵上的点的操作,往往都不必记录整个矩阵。当存不下时,考虑离线做法。

还有一些很厉害的减枝。能让复杂度不对的东西过,例如 P9974 [USACO23DEC] Candy Cane Feast B。

Description

Farmer John 的奶牛对甜食情有独钟,它们尤其喜欢吃糖果棒。FJ 共有 \(N\) 头奶牛,每头奶牛都有一个特定的初始高度。他想要喂它们 \(M\) 根糖果棒,每根糖果棒的高度也各不相同(\(1 \le N,M \le 2\cdot 10^5\))。

FJ 计划按照输入给出的顺序,逐一喂给奶牛们糖果棒。然后,奶牛们会按照输入给出的顺序一个接一个地排队,走向糖果棒,每头奶牛最多吃到与它高度相同的部分(因为它们够不到更高的地方)。即使奶牛吃掉了糖果棒的底部,糖果棒也在最初悬挂的地方保持不动,并不会被降低到地面。如果糖果棒的底部已经高于某头奶牛的高度,那么这头奶牛在它的回合中可能什么也吃不到。每头奶牛轮流吃过后,它们的身高会增加它们吃掉的糖果棒的单位数量,然后农夫约翰挂上下一根糖果棒,奶牛们再次重复这个过程(第一头奶牛再次成为第一个开始吃下一根糖果棒的)。

设每头牛的高度为 \(h_i\),每个糖的高度为 \(c_i\),保证 \(\forall h_i,c_i\in[1,10^9]\)。你可能需要使用 long long。

这玩意 \(n^2\) 做法是显然的。直接模拟复杂度是平方级别的。看上去肯定过不了。再考虑优化,由于每轮操作后,每头牛的高度是会变的。这个限制并不友好,除了直接模拟,没啥更好的方法。

这种情况下,该题很可能是诈骗题(指看上去不可做但非常简单,题目给的限制有点误导人的题)。此时应当注意一些特殊性质,比如数据范围。(ABC372E 就是诈骗题,注意到 \(k\le 10\) 后就可以乱做了。)本题我们还没用到的数据范围有 \(h_i,c_i\in[1,10^9]\)。考虑这个怎么用。

回顾一下暴力怎么做,对于每个糖果,按序扫所有的奶牛。按照糖果顺序进行模拟。这样做复杂度是严格的 \(O(nm)\)。

这样做太慢了,我们考虑一点优化。注意到在任意时刻,若糖果的高度为 \(0\),就没必要继续进行了。因为后面的都吃不到。加上这个减枝就过了。这就是诈骗所在。

我们考虑为什么这是对的。

注意到第一头牛永远是最先吃的,如果她把糖全吃完了就没后面牛什么事了,因此我们分讨她。

  • 她把糖全吃了。该情况发生当且仅当 \(a_1\) 大于糖的高度。时间复杂度 \(O(1)\)。

  • 她吃了高度为其身高的糖,即其身高 \(\times 2\)。此时继续扫后面的牛,时间复杂度 \(O(n)\)。

注意到每触发一次操作 \(2\),\(a_1\times 2\)。而 \(\max \{a_i\}=10^9\),最多触发 \(\log 10^9\) 次操作 \(2\),因此最坏复杂度 \(O(\log 10^9 n)\)。

诈骗题。

动态开点线段树初始传进去的应当是 1,而不是 0。否则根节点每次会被认为没开

P8674 [蓝桥杯 2018 国 B] 调手表

Source

有一个 \(n\) 个点的有向图,给定参数 \(k\)。对于 \(\forall i\in[0,n)\),有两条长度为 \(1\) 有向边,分别为 \(i\rightarrow (i+1)\bmod n,i\rightarrow (i+k)\bmod n\)。求 \(\max\limits_{i=0}^{n-1}\max\limits_{j=0}^{n-1}dis_{i,j}\),其中 \(dis_{i,j}\) 表 \((i,j)\) 之间的最短路。不难发现,若 $i\ne j $,则 \(dis_{i,j}\ne 0\) 必成立。

\(0<k<n<10^5\)。

Solution

题目看似是一个多源多点最短路问题。一种朴素的想法是直接跑 floyd,时间复杂度 \(O(n^3)\)。可获得 \(60pts\)。

多源多点最短路复杂度过高,且不易优化。注意到本题的连边方式具有统一性,我们尝试将其归纳成单源最短路问题。

本题的连边方式比较特殊,对于任意点 \(i\),都和 \((i+1)\bmod n,(i+k)\bmod n\) 连边。\(\bmod n\) 只是连成了环。本质和链是一样的。本题中,每个点的连边方式都是一样的。

注意到从 \(i\) 到 \(i+x\),\(i+1\) 到 \(i+x+1\) 的本质是一样的。换句话说,从任意点 \(x\) 开始,只要 \(\Delta\) 相同,答案都是一样的。考虑从任意点将环拆开,形成一条长度为 \(2n\) 的链。由于我们只是求最短路最大值,忽略点编号之间的差异,只考虑链的形态。不难发现都是一样的。

因此,从时刻 \(0\) 开始的单源最短路即可涵盖所有答案。由于 \(n<2\times 10^5\),我们可用 dijkstra 算法做到 \(O(n\log n)\),当然,由于边权都是 \(1\),也可用复杂度更优的 BFS,时间复杂度 \(O(n)\)。

参考代码(dijkstra vision)
constexpr int N = 1000010;
namespace solution
{
    typedef pair<int,int> PII;
    int n,k;
    vector <PII> Edge[N];
    int vis[N];
    int dis[N];
    priority_queue <PII,vector <PII>,greater<PII> > q; // priority_queue 默认降序排序,需要重定义或者传 -div[v] 这点需要注意
    void dijkstra()
    {
        #define x first
        #define y second
        memset(dis,0x3f,sizeof(dis));
        q.push(PII(0,0));
        dis[0] = 0;
        while(q.size())
        {
            PII p = q.top();
            q.pop();
            int u = p.y;
            if(vis[u]) continue; // 每个点最多入队一次,判重。
            vis[u] = 1;
            for(auto t:Edge[u])
            {
                int v = t.x,ww = t.y;
                if(dis[v] > dis[u] + ww)
                {
                    dis[v] = dis[u] + ww;
                    q.push(PII(dis[v],v));
                }
            }
        }
    }
    void solve()
    {
        cin>>n>>k;
        for(int i=0;i<n;i++) 
        {
            Edge[i].push_back(PII((i+1)%n,1));
            Edge[i].push_back(PII((i+k)%n,1));
        }
        dijkstra();
      //  cerr<<"nxt"<<endl;
        int maxn = 0;
        for(int i=0;i<n;i++) maxn = max(maxn,dis[i]);
        cout<<maxn<<endl;
        return;
    }
}

参考代码(bfs vision)
constexpr int N = 1000010;
namespace solution
{
    int n,k;
    vector <int> Edge[N];
    int dist[N];
    int vis[N];
    void bfs()
    {
        queue <int> q;
        q.push(0);
        vis[0] = 1;
        while(!q.empty())
        {
            int u = q.front();
            q.pop();
            for(auto v:Edge[u])
            {
                if(vis[v]) continue;
                vis[v] = 1;
                dist[v] = dist[u] + 1;
                q.push(v);
            }
        }
    }
    void solve()
    {
        cin>>n>>k;
        for(int i=0;i<n;i++)
        {
            Edge[i].push_back((i+1)%n);
            Edge[i].push_back((i+k)%n);
        }
        bfs();
        int maxn = 0;
        for(int i=0;i<n;i++) maxn = max(maxn,dist[i]);
        cout<<maxn<<endl;
        return;
    }
}
既然写到了 bfs,复习一下 01 bfs。

当一张图的边权只有 0/1,求最短路时,可使用 01 bfs 在 \(O(n)\) 的复杂度内求单源最短路。

具体的,开一个 deque,若当前边权为 \(1\),从队头入队,否则从队尾入队。这样入队保证了永远是 \(0\) 先扩散,再 \(1\) 扩散。由于 bfs 每个点只会被扫到一次,所以这么做是对的。

例题:P4554。代码也很好写。

P1330 封锁阳光大学

Source

有一个含有 \(n\) 个点,\(m\) 条边的无向图。(不保证连通)。控制一条边 \((u,v)\) 当且仅当控制点 \(u\) 或点 \(v\)。不可以控制两个相邻的点。求最少需要控制多少个点,才可以控制图上所有的边,。若无解输出 Impossible。

\(1\le n\le 10^4,1\le m\le 10^5\)。

Solution

复习简单二分图染色。

题目限制不可以控制两个相邻的点,且每条边都需要控制一个点。这是一个典型的二分图染色问题。每个点令其只能染 \(0/1\) 颜色。对于边 \(u\rightarrow v\),若 \(u\) 染色为 \(w_i(w_i\in{0,1})\),\(v\) 染色为 \(~w_i\)。这是题目限制。

我们又注意到,对于一个连通快 \(S_i\),确定连通快上任意一个点的颜色,就能确定连通快上所有点的颜色。这很显然,在一个连通快内,从任意一点出发遍历,总能遍历所有点。遍历到节点 \(v\) 时,依据其父节点 \(u\) 的染色即可确定唯一的 \(v\) 颜色,以此类推。同时,由于从连通快内任意一个点都可以将连通快划分为两个集合(如果不矛盾),这两个集合内的元素是唯一的,从哪个点开始染色结果都是一样的。

这个性质和 ABC373D很像,ABC373D 我们可以将有向图看作无向图,一个连通快内确定一个点的权值即可确定所有点权值。

注意到本题是不连通的,每个连通快都要扫一遍。对于每个连通快,取集合元素个数较小值即可。

参考代码
constexpr int N = 100010;
namespace solution
{
    int n,m;
    bool have_answer = 1;
    vector <int> Edge[N];
    int vis[N];
    int color[N];
    int num1 = 0,num2 = 0;
    void dfs(int u,int fat)
    {
        if(!have_answer) return;
        int need_ans = 0;
        if(color[fat]) {need_ans = 0;num1 ++;}
        else {need_ans = 1;num2 ++;}
        if(vis[u])
        {
            if(need_ans != color[u]) 
            {
           //     cerr<<"no answer"<<endl;
                have_answer = 0;
                return;
            }
            return;
        }
        color[u] = need_ans;
        vis[u] = 1;
        for(auto v:Edge[u])
        {
            if(!have_answer) return;
            if(v == fat) continue;
            dfs(v,u);
        }
    }
    void solve()
    {
        cin>>n>>m;
        for(int i=1;i<=m;i++)
        {
            int u,v;
            cin>>u>>v;
            Edge[u].push_back(v);
            Edge[v].push_back(u);
        }
        int ans = 0;
        for(int i=1;i<=n;i++)
        {
            if(!vis[i])
            {
                num1 = num2 = 0;
                dfs(i,0);
                if(!have_answer)
                {
                    cout<<"Impossible"<<endl;
                    exit(0);
                }
                ans += min(num1,num2);
            }
        }
        if(!have_answer) cout<<"Impossible"<<endl;
        else cout<<ans<<endl;
    }
}

历年真题的解析尽量写的详细点,尽可能的呈现出一个比较自然的思路,弄清楚本题的 trick 点在哪里。真题一般是比较综合的。

P9750 [CSP-J 2023] 一元二次方程

Source

省流:给定一个一元二次方程的一般形式 (即给定 \(a,b,c\)),求出该方程的较大解,若无解输出 NO。输出格式应当符合数学上的要求。

Solution

数学模拟题。要求模拟一个解方程的过程,题面描述很长,但大部分在描述输出格式问题。

本题在做前一定要先把思路搞明白,先设计好算法逻辑,明确每一步要干什么,再写。否则调的时候会很难受。

这里列举几个坑点。

  • 题目描述中的 "较大值" 并不一定是 \(\dfrac{-b+\sqrt{\Delta}}{2a}\),我们应当考虑 \(2a\) 符号问题,当 \(2a<0\) 时,\(-\sqrt{\Delta}\) 是更优的。当然,我们不必分讨。注意到无论何时,\(\dfrac{\sqrt{\Delta}}{2a}\) 一定是正的。直接绝对值处理即可。

分数化简应用分子,分母同除以最大公因数。这点显然。

最后符号需要处理干净。这一点写之前应当设计好。

参考代码
namespace solution
{
	int T,M;
	int a,b,c;
	bool flag = 1;
	void frac(int m,int n)
	{
		flag = 0;
		if(m * n < 0) cout<<"-";
		m = abs(m),n = abs(n);
		while(__gcd(m,n) != 1)
		{
			int t = __gcd(m,n);
			m /= t,n /= t;
		}
		if(n == 1) 
		{
			flag = 0;
			cout<<m;
		}
		else { flag = 0;cout<<m<<"/"<<n;}
		return;
	}
	void solve()
	{
		cin>>a>>b>>c;
		int delta = (b*b)-(4*a*c); // get_delta
		if(delta < 0) // no_answer
		{
			cout<<"NO"<<endl;
			return;
		}
		double sq_delta = sqrt(delta); // check有理数
		if((int)sq_delta * (int)sq_delta == delta)   
		{
			if(2*a < 0) frac(-b-(int)sq_delta,2*a);
			else frac(-b+(int)sq_delta,2*a);
			cout<<endl;
			return;
		} 
		if((double)(-b*1.0/(2*1.0*a*1.0)) != 0) frac(-b,2*a);
		int res = 1;
		int delta_cpy = delta;
		for(int i=sqrt(delta);i>=1;i--) 
		{
			if(delta % (i*i) == 0) 
			{
				res *= i;
				delta /= (i*i);
				break;
			}
		}
		delta_cpy /= (res*res);
	    if(double(b*1.0/a*1.0) == 0) {goto nxt;} // 没有这一项
		if(!flag)cout<<"+";
		nxt:
		a = abs(a);
		int aa = 2*a;
		while(__gcd(res,aa) != 1)
		{
			int t = __gcd(res,aa);
			res /= t;
			aa /= t;
		}
		if(res != 1) cout<<res<<"*";
		cout<<"sqrt("<<delta_cpy<<")";
		if(aa != 1) cout<<"/"<<aa;
		cout<<endl;
		return;
	}
	void mian()
	{
		cin>>T>>M;
		while(T--) solve();
	}
}

P9751 [CSP-J 2023] 旅游巴士 & 复习最短路径算法相关

Source

有一张 \(n\) 个点,\(m\) 条边的有向图,你需要在图上从 \(1\) 号点走到 \(n\) 号点。每走过一条边,耗费 \(1\) 单位时间。你的起始时间和终止时间必须是 \(k\) 的倍数。(\(k\) 为给定参数),并且对于每个点有一个限制 \(a_i\),该点可走当且仅当到达该点的时间 \(t\ge a_i\)。求从起点到终点的最短时间。

$ 2\le n\le 10^4,1\le m\le 2\times 10^4,1\le k\le 100,1\le u_i,v_i\le n,0\le a_i\le 10^6$。

Solution

注意到本题本质是 边权为 \(1\) 的最短路问题。只是加入了两个限制条件:起始时间和终止时间必须是 \(k\) 的倍数;到达每个点都有一个限制 \(a_i\)。

两个限制我们一个一个解决。注意到解决限制 \(1\) 即可获得 \(35\) pts。我们先解决限制 \(1\)。

限制 \(1\) 要求起始时间和终止时间必须是 \(k\) 的倍数,求最短路时我们不好控制终止时间,起始时间相对好处理。考虑转化,上述限制等价于 起始时间和行走时间都是 \(k\) 的倍数,这个转化比较显然。

上述处理都是基于直觉和经验的判断,我们还不知道这个题怎么做。

注意到 \(k\le 100\),这种特殊数据范围一定要注意。例如 ABC372E,这个题注意到 \(k\le 10\) 就可以随便做了。不必用什么高级数据结构。还有 ABC355F,\(c_i,w_i\le 10\)。

众所周知,在 dp 中,一般的,多一个限制意味着止多一层状态(在能开下的前提下)。因为我们需要多开一层状态来记录限制。这个思想在 P10236 yLcpc2024 排卡 中有所体现。以及 P4568 JLOI 飞行路线 有免费次数的限制,那就多开一层状态来转移免费次数。其实这就是 dp。不止 dp,在 几乎所有最优化问题(除了贪心)中,该思想都非常重要。本题同理。

在本题中。要求起始时间和行走时间都是 \(k\) 的倍数。若忽略限制 \(2\),从 \(0\) 时刻开始走一定是最优的。我们只需要考虑行走时间必须是 \(k\) 的倍数。借鉴上述思想,我们多开一层状态记录行走时间。直接记录原始时间显然不现实,既存不下又不好统计答案。我们考虑状态合并。状态合并需考虑 状态合并后对答案是否有影响。注意到对于点 \(i\),在时间 \(t_0,t_1\) 都可以走到,且满足 \(t_0\equiv t_1(\bmod k),t_1>t_0\)。则 \(t_1\) 是不可能成为最优解的。题目只要求行走时间是 \(k\) 的倍数。我们从 \(0\) 时刻开始走,行走时间是 \(k\) 的倍数等价于终时间是 \(k\) 的倍数。设走到点 \(i\) 后,最优方案再走 \(x\) 步后即可到达 \(n\),且 \(t \bmod k =0\),\(t_1\) 一定比 \(t_0\) 劣。由特殊到一般,推广该结论。每个节点只需要记录在 \(\bmod\space k\) 意义下时间即可。这样合并就能存下了。

只考虑限制 1 代码
const int N  = 10010;
const int INF = 0x3f3f3f3f;
typedef pair<int,int> PAIR;
vector <PAIR> Edge[N];
int n,m,k;
int dist[N][N/2]; 
void bfs()
{
	queue <PAIR> q;
	q.push(PAIR(1,0));
	dist[1][0] = 0;
	while(!q.empty())
	{
		auto [u,i] = q.front();
		q.pop();
		for(auto [v,w]:Edge[u])
		{
			int j = (i+1)%k;
			if(dist[v][j] == -1)
			{
				dist[v][j] = dist[u][i] + 1;
				q.push({v,j});
			}
		}
	}
}

考虑到这里,我们解决了 \(a_i=0\) 的特殊性质,获得了 \(35\) pts。

接下来考虑限制 \(2\),要求走到点 \(i\) 时行走时间至少为 \(a_i\)。且一旦开始走就不能停下。该限制满足使我们不得不在开始时停留。即修改开始时间。朴素的想法是令每个 \(k\) 的倍数作为开始时间,跑上述算法。复杂度是 \(n^2\) 级别的,不能通过。

换位思考,不妨先直接走,用类似反悔贪心的思路,走到走不动时再改开始时间。这个思路在 CSP-J 2023 公路 用过。初始令开始时间为 \(0\),当走到一个点,当前时间为 \(t,t<a_i\),初始时间 \(s\) 至少要加 \(\lceil\dfrac{a_i-t}{k}\times k\rceil\) 时间,即最少需要加多少个 \(k\) 才能走。这。朴素的想法是令每个 \(k\) 的倍数作为开始时间,跑上述算法。复杂度是 \(n^2\) 级别的,不能通过。这样做每走一步时间不是严格 \(+1\) 了(在实现上考虑),直接 bfs 会假,我们需要让时间少的先松弛,就是 dijkstra 算法。

这个东西也可理解为,对于每条边 \((u\rightarrow v)\),有一个和 \(u\) 有关的边权。对这样一张图跑单源最短路,就是 dijkstra 算法。我们转移的是状态,这个状态想要继续走,必须要修改时间。

参考代码
void bfs()
{
	priority_queue <Node> q;
	dist[1][0] = 0;
	q.push({1,0,dist[1][0]});
	while(!q.empty())
	{
		int u = q.top().u,i = q.top().i;
		q.pop();
		if(vis[u][i]) continue;
		vis[u][i] = 1;
		for(auto [v,w]:Edge[u])
		{
			int p = dist[u][i];
			int j = (i+1)%k;
			if(p < w) p+=(w-p+k-1)/k*k;
			if(dist[v][j] > p+1)
			{
				dist[v][j] = p+1;
				dist[v][j] = p+1;
			//	cout<<dist[v][j]<<endl;
				q.push({v,j,dist[v][j]});
			}
		}
	}
}

做完这个题顺便复习一下最短路算法。重点复习 SPFA /dijkstra/ floyd。

floyd 的功能远不止求最短路。反之,求最短路是最低级的用法。floyd 可以求传递闭包。最小环,利用其特性还可分治。

dijkstra 基于贪心。其本质是维护一个点集 \(S\) 表示已经确定最短路的集合。每个点仅可松弛一次。其不可解决负权边。因为其算法逻辑为每次找当前距离源点最近的点,进行松弛。若边权为负,松弛完的点比当前点距源点还近,矛盾。

SPFA 和 dijkstra 比较相似,但它是暴力松弛。每个节点可以进出队多次。但每次松弛至少有一个点的最短路确定。即每个点最多入队 \(n-1\) 次,超过 \(n-1\) 次即出现了负环。无解。这就是 SPFA 判负环的原理。还有一种做法,记录从起点走到当前节点所经过的节点个数。如果走到当前节点经过的节点个数 \(>n\),那就一定走到一个负环上了。这种方法容易拓展成求正环。只需求最长路即可。

我的 dijkstra & spfa 模板:Link

最小环有个性质:设最小环上有三个点 \(a,b,c\),有边 \((a\rightarrow b),(b\rightarrow c)\),\(c\) 到 \(a\) 之间有若干点。断掉边 \(a\rightarrow b\),则剩下的链一定是 \(a\) 到 \(b\) 的最短路。(不考虑边 \(a\rightarrow b\))。证明可以用反证法。

floyd 求最小环利用了其动态规划思想,具体的,\(f_{i,j,k}\) 表示 \(i\rightarrow j\) 只经过 \([1,k]\) 的最短路。我们认为,最小环至少有 \(3\) 个点,枚举环上最大点 \(k\)。则以 \(k\) 为环上最大点,另外两个点为 \((i,j)\) 的最小环,一定是由 \(i\) 到 \(j\) 只经过区间 \([1,k)\) 内的点的最短路,边 \(i\rightarrow k\),边 \(k\rightarrow j\) 组成。这是上述结论的应用。注意到在 floyd 算法中,当外层循环枚举到 \(k\),且第 \(k\) 次松弛尚未开始,\(f_{i,j}\) 表示 \(i\) 到 \(j\) 仅经过 \([1,k)\) 内的点的最短路。这恰好符合上述要求。因此,在枚举到 \(k\) 时,我们先更新 以 \(k\) 为环上最大顶点的最小环,再更新最短路。

时间复杂度 \(O(n^3)\)。

根据 floyd 的这个性质,还有一个 trick 叫做 分治 floyd。仅针对特定题目好用。例如 百度地图的实时路况那个题。这玩意就是 cdq 分治。有时间可以简单了解下原理。

P8817 [CSP-S 2022] 假期计划

Source

有一张 \(n\) 个点,\(m\) 条边的无向图。图上所有边路程为 \(1\)。定义两个点 \((u,v)\) 可达当且仅当 \((u,v)\) 的最短距离 \(\le k\)。你需要选择 \(4\) 个点,命名为 \(A,B,C,D\),满足 \((1,A),(A,B),(B,C),(C,D),(D,n)\) 是可达的。求四个点权值和最大值。

\(5\le n\le 2000,1\le m\le 10000\)。

Solution

注意到 \(n\le 2000\),且所有边的路程为 \(1\),且题目限制和两点间的最短路有关。我们可以在 \(n^2\) 的复杂度内求出全源最短路。具体的,从每个点开始跑 bfs 即可。顺便可以处理出每个点可达的点,这就解决了限制,且复杂度合法。当不知道题目如何做时可以先从小限制开始解决。因为以后无论是暴力,还是正解,都需要解决限制。

首先考虑最朴素的暴力,由于我们只取四个点,直接 \(n^4\) 枚举组合。由于前面预处理了两个点是否可达,\(n\) 最大才 \(2000\),这层关系二维数组存储即可。check 组合是否合法,取最大值即可。时间复杂度 \(O(n^4)\),可以获得 \(60\) pts。事实上,\(60\) pts 的性价比已经很高了。赛时做到这里已经可以扔掉 T1,开 T2 了。

分析数据范围,\(n\le 2000\) 要求我们在 \(n^2\) 的复杂度内解决问题。注意到题目本质要求 四个点的总价值和最大。这种最优化问题,要么 DP,要么贪心。DP 并不好设计状态。考虑贪心。

一种直观的想法是,从起点开始贪,贪可以到达的点中价值最大的点 \(u\)。然后从 \(u\) 开始继续贪,以此类推。这么贪显然是错误的,一方面,可能最后到不了 \(n\),另一方面,太过局限性。

既然这种贪心是错误的,我们应当反思一下为什么是错误的,即它为什么具有局限性。考虑一下,我们从起点 \(1\) 贪心地选择了 \(v\),而对于同样可达的 \(u\),接下来到达的点权最大值可能远大于 \(v\),这是贪心假掉的主要原因。(不考虑能否走到的问题,分析这个意义不大。)因此,贪心假掉的主要原因在于选择了 \(v\) 后还会选择元素。选择 \(v\) 以外的元素后面造成的价值可能更大。这个原理和数字三角形贪心假的原因基本是一致的。不满足无后效性。

那我们选择了 \(v\) 后不选择元素了呢?这样贪心就对了。且这种情况在本题中是显然成立的。(已经选择了 \(A,B,C\),选择 \(D\) 时可贪心选择 \(n\) 也可达的,价值最大的,很显然,选 \(D\) 后不会再影响其他元素的选择。)复杂度直接降到 \(O(n^3)\)。类似的,环是具有对称性的。选择了 \(B,C,D\) 后,再贪心地选 \(A\) 也是合法的。这样复杂度就是 \(n^2\) 了,我们只需要枚举 \(B,C\),贪心选 \(A,D\) 即可。

因此,我们预处理出每个点和 \(1\) 号点,\(n\) 号点均可达的点中,价值最大的点。预处理复杂度是 \(n^2\) 级别的。但这样会有问题,例如点 \(B\) 和点 \(1\) 都可达的,且价值最大的点是 \(C\),或者点 \(C\) 和点 \(n\) 都可达的,且价值最大的是 \(B\)。这样的问题我们在树形 dp 中也见到过。(求树上一个点最远到达哪个点。)当时我们的处理方法是记录点 \(u\) 向下最远,第二远能到达哪个点。这里也是同理。

但是,记录最大和次大就够了吗?上面提到,贪心取 \(A\) 和贪心取 \(D\) 的过程都可能造成矛盾。若贪心取到的,\(B,1\) 都可达,且价值最大的点 \(A\) 就是 \(C\),我们选第二大的点 \(k\),这样贪没有任何问题。再考虑对 \(C\) 进行贪心,\(C,n\) 可达且价值最大的点恰好是 \(B\),此时选第二大的点,又恰好是 \(k\),这就有问题了。

为了杜绝上述情况发生,我们需要记录前三大。记录前三大,当第二大之间出现矛盾时,我们可以决策,取 \(B\) 的第二大,\(C\) 的第三大优,还是取 \(C\) 的第二大,\(B\) 的第三大优。这样写就麻烦了。注意到无论什么情况,都是 \(b,c\) 的第一大,第二大,第三大两两组合进行比较。不如直接列举 \(3\times 3\) 种情况,检查互异性直接算就好。这样大大降低了代码实现难度。

然后就做完了。本题有两个难点,一个是贪心,只枚举 \(B,C\),贪心取 \(A,D\) 是无影响的。以及对于冲突的处理。这里处理方式和树形 dp 是类似的。

参考代码(用 2024 年的码风重构了一遍)
constexpr int N = 8010;
constexpr int INF = 1e11;
namespace solution
{
    int n,m,k;
    int val[N];
    bool vis[N][N];
    vector <int> Edge[N];
    vector <int> arrive[N];
    bool cmp(int a,int b){return val[a] > val[b];}
    queue <int> q;
    int countt[N];
    void bfs(int s) // 以每个点为起点跑全源最短路
    {
        int dist[N];
        int visit[N];
        fill(visit+1,visit+n+1,0);
        fill(dist+1,dist+1+n,INF); 
        q.push(s);
        dist[s] = 0;
        visit[s] = 1;
        while(!q.empty())
        {
            int u = q.front();
            q.pop();
            if(u == s) goto nxt; 
            vis[s][u] = 1;
            if(vis[1][u]&&u != s)
            {
                arrive[s].push_back(u);
            }
            sort(arrive[s].begin(),arrive[s].end(),cmp);
            if(arrive[s].size() > 3) arrive[s].pop_back();
            nxt:
            if(dist[u] >= k) continue;
            for(auto v:Edge[u])
            {
                if(visit[v]) continue;
                dist[v] = dist[u] + 1;
                q.push(v);
                visit[v] = 1;
            }
        }
    }
    void solve()
    {
        cin>>n>>m>>k;
        for(int i=2;i<=n;i++)
            cin>>val[i];
        for(int i=1;i<=m;i++)
        {
            int u,v;
            cin>>u>>v;
            Edge[u].push_back(v);
            Edge[v].push_back(u);
        }
        for(int i=1;i<=n;i++) bfs(i);
        int ans = 0;
        for(int b = 2;b <= n;b++)
        {
            for(int c = 2;c <= n;c ++)
            {
                if(b == c) continue;
                if(!vis[b][c]) continue;
                for(auto a:arrive[b])
                {
                    for(auto d:arrive[c])
                        if(a != d && a != c && b != d) ans = max(ans,val[a]+val[b]+val[c]+val[d]); 
                }
            }
        }
        cout<<ans<<endl;
        return;
    }
}

[CSP-S 2022] 策略游戏

[CSP-S 2023] 密码锁

有一个自行车密码锁,共有 \(5\) 个拨圈。每个拨圈上是从 \(0\) 到 \(9\) 的数字。即 \(9\) 拨动一个位置可以变成 \(0\) 或 \(8\)。其实就是正常密码锁的构造。

这个密码锁有一个密码,小 S 每次使用正确密码锁车后,会进行下述任意一个操作来打乱密码。

  • 按照随机方向随机拨动一个拨圈随机次。

  • 按照随机方向随机拨动 两个相邻的拨圈随机次。

很不幸的是,愚蠢的小 S 忘记了密码。但他拥有 \(n\) 个密码打乱后的序列。求有多少种可能的密码,满足这 \(n\) 个序列。

\(n\le 8\)。

Solution

CSP-S 2023,看到题目感觉不可做。直接冲 T2,但是没冲出来。30 pts 遗憾离场。在 2024/10/06,VP CSP-S 2023,独立做了出来。并且 VP 获得了 150 pts。哎。

乍看感觉不大可做的样子。

我们又注意到 \(n\le 8\),且只有 \(5\) 个拨圈。这个数据比我们见过的任何一个题都小。考虑到 CCF 如果要考察算法,思维的话,完全可以改个题目背景,加强一下数据。但是并没有,很明显 CCF 想要放各种复杂度“爆炸”的做法过的。所以我们要大胆想。

既然题目问有多少种合法的密码是满足所给限制的。如果我们根据限制算密码数量,可能要考虑到重复性问题,即需要容斥一下。不妨换位思考,考虑直接枚举每一种密码可能性。然后判定是否合法即可。这样就把计数问题转换为判定性问题。

至于如何判定,有多种写法,总之直接模拟即可。写一下犯的错误:

  • 读题不清楚,题目要求转任意次,但是第一次写成了只转一次,一直过不了样例。

  • 转连续两个时,判定是否合法一般是确定第一个怎么转,第二个按照相同的方式转。但是由于密码锁转动可以看作一个环,正着转和反着转都是可以的,因此判断第二个时,正着转和反着转都要考虑,不然会少统计。

调试技巧:当大样例很弱,且不好对拍时,可以自己手玩一些数据,比如对于上述第二个错误,手动构造若干个 \(n=1\) 的数据,可以很容易发现答案并不恒等于 \(81\),这显然是不合法的。

这个方法写的代码很长,但是几乎没有思维含量。代码太长了,直接扔剪切板了。

Code

[CSP-S 2023] 消消乐

有一个长度为 \(n\) 的字符串序列,一次操作可以消除两个连续的,相同的字符。求有多少个子序列是“可消除的”。

\(n\le 2\times 10^6\)。

对于 \(50\%\) 的数据,满足 \(1\le n\le 8000\)。

Solution

注意到字符串消除规则:每次消除相邻的两个相同字符。且删除字符后,中间的空位会消失,即剩下的字符串会拼到一起。我们首先考虑如何实现这个过程。注意到这个东西和括号匹配很像。我们把第一个出现的字符看作左括号,第二个出现的字符看作右括号(相对),这就变成了括号匹配问题。括号匹配问题通常用栈解决。要有这个敏感度,遇到这种删除后,剩下的会拼到一起的问题,首先考虑栈。这么做不能保证复杂度是对的,但是至少能解决部分分,算法正确性大概是对的。

当正解一时半会想不到时,首先考虑暴力怎么做。注意到子序列必须是连续的,上文提到可以用栈来模拟这个删除的过程。对于统计子序列,一种比较自然的想法是固定左端点,考虑有多少个右端点符合题意。(因为子序列是必须连续的。)

当固定左端点 \(i\) 时,直接用栈开始模拟删除操作。若扫到某个节点 \(j\) 时,栈为空,证明从 \(i\) 到 \(j\) 的字符串是 “可删除的“。统计答案。枚举每个左端点向右扫即可。时间复杂度 \(O(n^2)\)。可通过 \(50\%\) 的数据。做到这里,如果你写 T1 很认真,没挂的话,你已经获得了 \(150\) 分,获得了一等奖和蓝钩。

回顾一下上面 \(50\) 分的部分分。我们首先注意到这题本质上就是个括号匹配,根据现有知识,括号匹配用栈做,于是这个也用栈做。(事实上,这种删除后剩下的会拼到一起,以及在 dfs 一棵树,在遍历过程中,求当前已经遍历到的,还没被回溯的点(当前遍历到的链),都用栈。不要忘了 dfs 的本质就是调用系统栈。)。我们又注意到,子序列必须是连续的。常用的统计策略是固定左端点,向右扫。扫到合法的右端点再统计。

接下来考虑线性(或者带个 \(\log\))怎么做。

线性就不能用栈了,不能模拟这个删除过程。你从第一个字符开始用栈模拟是没道理的。因为你无法考虑中间几个合法的子区间,拼接后也是合法的情况。

考虑观察一下性质,注意到一个字符串是合法的,当且仅当满足下列两条件任一。

  • 若 \(A\) 合法,则 \(xAx\) 必合法。

  • 若 \(A\) 合法,\(B\) 合法,则 \(AB\) 必合法。

且一定有至少一对初始就相邻的字符。否则必无解。毕竟连第一步都删不了。

观察到上述关系具有传递性,考虑 dp。设 \(f_i\) 表示以 \(i\) 为结尾的字符串有多少个是“可删除的”。定义 \(g_i\) 表示从 \(i\) 向左第一个位置,使得 \(g_i\sim i\) 是“可删除的”。转移方程即为 \(f_i=f_{g_i-1}+1\)。

为什么要这么设计状态呢?注意到上述操作满足“可合并性”。具体的,若 \(A\) 合法,\(B\) 合法,则 \(AB\) 必合法。因此,我们对于每个右端点,扫到左边第一个 \(j\),满足 \(i\sim j\) 是合法的。即 \(j\) 是以 \(i\) 为结尾,第一个合法的左端点。我们认为,\(j\) 左边的任意一个左端点,和 \(i\) 组合,不合法。

\(g_i\) 怎么求呢?由于我们钦定,\(g_i\) 是最大的能使 \(g_i\sim i\) 成为合法字符串的位置。故一定满足 \(c_{g_i}=c_i\)。这也比较显然。具体的,初始化 \(g_i=i-1\),每次暴力跳,令 \(g_i=g_{g_i}-1\)。直到满足 \(c_{g_i}=c_i\)。\(g\) 的转移也运用了动态规划思想。

跳的时候注意特判,如果根本不存在以 \(i\) 结尾的合法序列,会不断跳,如果跳到负数,那就无解。

回顾本题思路:我们首先注意到了本题合法的字串满足可加性,且要求接近线性做法,启发我们设计状态进行 dp。接下来顺着可加性往下思考,发现要利用可加性进行转移,以字串结尾列状态比以字串开头列状态容易转移。这也是 dp 列状态的常见思路,一般都是从后往前找。设计出状态后,考虑转移。转移还是利用可加性。找到以 \(i\) 为结尾的,第一个合法的左端点 \(j\),\(f_i=f_{j-1}+1\)。对于计算 \(j\),一种暴力的思路是直接用栈模拟,但最坏能退化到 \(n^2\) 级别(当答案为 \(0\) 时。)不能通过。但感觉这个做法比严格 \(n^2\) 暴力拿的分多。在随机数据下是严重跑不满的。

考虑上述暴力求 \(g\) 的方法,因为我们是顺序求,当 \(i-1\) 已经无解,求 \(i\) 时,跳到了 \(i-1\),且 \(c_i\ne c_{i-1}\),则 \(i\) 也必定无解。没必要继续往下算了。继续算只会重复相同的步骤,得到和 \(i-1\) 一样的结论。这属于可行性减枝。加上这个在某些数据(例如给定字符串 \(s\) 两两元素互不相同时)会跑的很快。但如果 CCF 精心构造数据也能被卡(例如我可以构造一种每次跳到最后才发现无解的情况。这样每次都得重复跳很多次,我当然也可构造都有解的情况,这样可行性减枝就失效了。)。

发现只有可行性减枝不太行,注意到上述跳法,无论加不加减枝,都是一次只跳一步。复杂度上界都是 \(n^2\),无论怎么优化。想要把一个 \(n^2\) 的东西优化过 1e6 听起来就不太现实。一次跳一步太慢了,不妨一次跳多步。上文提及,一个字符串有解要么形如 \(xAx\),要么就是两个合法字符串拼接。当我们跳到了一个 \(k\),已经有了答案 \(j_k\),那我们可以直接跳到 \(j_k\),不难发现,\(i\) 的答案 \(j_i\) 不可能小于 \(j_k\),因为必须保证以 \(k\) 为结尾字符串有解。(依据性质 1),这又是个 dp。

参考代码
namespace solution
{
    char ch[N];
    void solve()
    {
        int n;
        cin>>n;
        for(int i=1;i<=n;i++) cin>>ch[i];
        long long f[N],g[N];
        for(int i=1;i<=n;i++) g[i] = i-1;
        for(int i=1;i<=n;i++)
        {
            while(g[i] >= 0 && ch[g[i]] != ch[i] && g[i] > 0) g[i] = g[g[i]]-1;
        }
        for(int i=1;i<=n;i++) if(g[i] > 0) f[i] =  f[g[i]-1] + 1;
        long long sum = 0;
        for(int i=1;i<=n;i++) sum += f[i];
        cout<<sum<<endl;
    }
}

打模板犯的错误 & 部分算法总结

练习一下动态开点 segment tree/ 以及 BST /权值线段树。

列一下容易出错的地方。

  • pl <= mid pr > mid 两个区间不能有交集。即不能写成 pl <= mid pr >= mid。

  • 初始传进去的 p 从 \(1\) 开始,\(tot\) 应初始化为 \(1\)。这样再 \(++tot\) 新开节点是 \(2\) 而非 \(1\)。

单调队列“退役”操作应当是 i - q.front() +1> k,注意区间长度的表示。且先退役,再更迭。

然后就是 ST 表,它通常用于解决 可重复贡献问题,例如 RMQ,区间 \(\gcd\)。ST 表解决的问题必须满足结合律。可以理解为必须是 可合并的。例如,RMQ 问题就具有结合律,我们求区间 \([L,R]\) 的 RMQ,可以将区间 \([L,R]\) 拆分成两个区间。这两个区间不必没有交集,两区间的并集等于区间 \([L,R]\) 即可。同时,并查集的合并也满足结合律。将两个集合 \(A,B\) 合并,令 \(C,E/D,F\) 分别是 \(A,B\) 的子集。我们可以先把 \(C,D\) 合并,产生新集合 \(G\);再把 \(E,F\) 合并,产生新集合 \(H\)。最后合并 \(H,G\) 即可。典型应用有 萌萌哒。写 ST 表的时候,尽量封装起来,这样当开好几个 ST 表时,不必写重复的代码。

具体的,每次初始化 \(f\) 数组,应当注意的是,先枚举长度,再枚举元素。枚举元素的时候根据长度确定边界。因为 \(f\) 转移时是从小区间转移到大区间的。\(f_{i,j}\) 表示 \(i\) 跳 \(2^j-1\) 步的答案。因此,\(f_{i,0}=a_i\)。

前面提到,由于 \(f_{i,j}\) 表示是跳 \(2^j-1\) 步,因此 init 枚举 \(i\) 时边界应当为 i+1<<j-1<=n。至于转移,就是运用了倍增思想。

至于树状数组,可以说利用了 二进制拆分思想。lowbit 实际上是每次去掉 \(x\) 的最后一位。可以认为树状数组的原理是 唯一分解定理。也可以说是二进制拆分吧。构造出树状数组后,下面的节点所包含的范围一定是上面节点的真子集,\(c_x\) 必真包含于 \(c_{\text{lowbit}_x}\)。运用这个性质,我们不断向上跳 \(\operatorname{lowbit}_x\) 即可完成单点修改。树状数组是一种短小精悍的 ds,它经过一些复杂的改造,可以完成一些很复杂的操作,但是可拓展性还是不如线段树。需要注意,树状数组一般只能维护 可差分 的东西。

树状数组比较常用的是 求逆序对,很简单,在值域上建树状数组,倒着扫数组 \(a\),每次查 \(a_i-1\) 到 \(1\) 有多少个数。就是和 \(a_i\) 组成逆序对的数。这些数比 \(a_i\) 小,还出现在 \(a_i\) 后面,就和 \(a_i\) 组成逆序对。然后在树状数组上对 \(a_i\) 单点 \(+1\) 即可。值域往往很大,需要离散化。当然归并排序求更优,但是不如 BIT 好写。

写哈希的时候,gethash 的时候应当是 hash[r]-hash[l]*h[r-l+1],而不是 hash[r]-hash[l]*(r-l+1)。这个错误犯了两遍了!!!

倍增预处理应当先枚举 len,再枚举 n,因为转移方程 \(f_{i,j}=f_{f_{i,j-1},j-1}\) 是基于 \(j-1\) 转移的。树上倍增按照 dfs 的顺序转移即可。毕竟都是向上跳。

P3915 树的分解

Source

给出一棵 \(n\) 个点的树和 \(k\),求能否将树划分成 \(\dfrac{n}{k}\) 个连通快,且每个连通快的点数都为 \(k\)。

多测,共有 \(T\) 组数据,\(T\le 10,n\le 10^5\)。

Solution

看上去不太可做的样子。这是判定性问题,判定型问题用 dp 的情况挺少见的。

原来做过一个题,叫做 “小信爬树”,大概题意是这样的。

有一棵树,树上有 \(n\) 个节点.每个节点都有点权。我们希望通过若干次操作,满足对于任意点权 \(a_i\),都有\(l_i\le a_i \le r_i\)。每次操作如下。

  • 选择某个顶点 \(v\)。
  • 令 \(b_1,b2_\dots b_k\) 是从顶点 \(1\) 到顶点 \(v\) 的路径上的原点(即 \(b_1=1,b_k=v\) 并且 \(b_i\) 是 \(p_{b_i+1}\))。
  • 选择一个长度为 \(k\) 的非递减数组 \(c\),其中 \(c_1\le c_2\le \dots c_k\) 保证数组元素都是非负整数。
  • 对于每个 \(c_i(i\le i\le k)\),给 \(a_{b_i}\) 加上 \(c_i\)。
    求至少需要多少次操作才能完成这个调整。
    对于 \(100\%\) 的数据,满足 \(2\le n \le 2\times 10^5,1\le l_i\le r_i\le 10^9\)。

这个题乍看也是感觉不可做,\(n\le 10^5\),并且也不好设计状态。只能考虑树上贪心。因为要求最少操作,且注意到对下面的进行操作,会影响到上面的节点。而且这种影响并不是一个固定值,相反,对下面节点进行操作所加的值 \(a_i\),是上面的点所“沾光”的上界。考虑到这里,我们发现,在符合题意的前提下,对下面的点操作值越大,答案都是更优不劣的。且这种关系最多传递一层。因此,对于节点 \(u\),首先考虑继承它所有的儿子操作值,如果全部继承得到的点权 \(\ge\) \(u\) 要求的点权上界,则 \(u\) 光”沾光“就够了。它不需要进行操作即可满足题意。同时,它对父亲做出其点权上界减原点权的贡献。(原因显然,它只继承了儿子这些。最多向上传递这些。)反之,\(u\) 需要进行操作,既然操作了,那就在符合题意的基础上,对上面做出尽可能多的贡献。仍然是加到点权上界,对上面做出点权上界-原点权的贡献。

本题特点和小信爬树类似,既不好设计状态,又操作特别灵活。一般的,这类题目往往用贪心求解。

树上贪心一般从叶子开始贪,因为每个儿子对应唯一的父亲,而每个父亲对应若干个儿子。显然前者更好决策。树上连通块一定是在以某个节点为 root 的子树里。我们从叶子开始取子树,即在回溯时取,当回溯到 \(u\),下面恰好取了 \(k\) 个点,则分为一个连通快。很显然,如果不这么做后面的都不符合题意。若回溯到 \(u\),下面取了超过 \(k\) 个点,则无解。这也很显然。例如当 \(k=5\) 时,

img

显然,不可能形成大小为 \(5\) 的连通块。这就对应上面提到的无解情况。

当回溯到 \(u\),下面取了少于 \(k\) 个点时,向上传递答案继续取即可。这就非常优美。

由此看来,对于树上决策问题,从叶子开始,决策往往都是唯一的。毕竟每个儿子只对应一个父亲。

P1025 [NOIP2001] 数的划分

将整数 \(n\) 分成 \(k\) 份,每份不能为空,任意两个方案不相同。(不考虑顺序。例如 \((1,1,5),(1,5,1)\) 被认为是相同的。求有多少种不同的分法。

\(6< n\le 200,2\le k\le 6\)。

Solution

注意到 \(n\) 比较小。不妨直接考虑暴搜。加上剪枝一般能获得可观的分数。如果剪枝比较厉害也可能过掉。反正当暴搜遇上剪枝,复杂度一般比较玄学。

DFS 大暴搜题,一定要先想明白怎么做,把每一步都列明白,再写。一是为了梳理思路,别 dfs 时自己把自己绕进去,考虑好自己 dfs 需要传什么参;二是考虑能否剪枝,能写的剪枝一定都要写,不知道数据的情况下可能一个小剪枝会影响很多点。

还有就是,对于本题而言,它的本质是搜排列。我们无非就是搜出所有的排列顺序(当然中间得有可行性剪枝。)然后判定是否合法。有一个搜排列的常用技巧。时刻保证搜的排列是严格不下降的,这样一来减少了搜索的次数,又省去了判重。这个需要特别注意。题目不要求我们输出排列,我们就不需要记录排列。反正我们不需要判重。因此,我们传的参只有 剩余未被分的数,已经分的组数量,前面排列的最大值(排列必须严格不下降),然后就做完了。

参考代码
namespace solution
{
    int n,k;
    int res = 0;
    void dfs(int last_num,int divide_num,int maxn)
    {
        if(divide_num == k)
        {
            if(last_num) return;
            res ++;
            return;
        }
        if(last_num <= 0) return;
        for(int i=maxn;i<=last_num;i++) 
            dfs(last_num-i,divide_num+1,i);
    }
    void solve()
    {
        cin>>n>>k;
        for(int i=1;i<=n;i++) dfs(n-i,1,i);
        cout<<res<<"\n";
        return;
    }
}

P1124 文件压缩

该算法具体如下:对一个长度为 \(n\) 的字符串 \(S\),首先根据它构造 \(n\) 个字符串,其中第 \(i\) 个字符串由将 \(S\) 的前 \(i-1\) 个字符置于末尾得到。然后把这 \(n\) 个字符串按照首字符从小到大排序,如果两个字符串的首字符相等,则按照它们在 \(S\) 中的位置从小到大排序。排序后的字符串的尾字符可以组成一个新的字符串 \(S'\) ,它的长度也是 \(n\),并且包含了 \(S\) 中的每一个字符。最后输出 \(S'\) 以及 \(S\) 的首字符在 \(S'\) 中的位置 \(p\)。

举例:\(S\) 是 example

  1. 构造 \(n\) 个字符串。
example
xamplee
ampleex
mpleexa
pleexam
leexamp
eexampl
  1. 将字符串排序。
ampleex
example
eexampl
leexamp
mpleexa
pleexam
xamplee
  1. 压缩结果。

\(S' = \texttt{xelpame}\),\(p = 7\)

请你编一个程序,读入 \(S'\) 和 \(p\),输出字符串 \(S\)。

保证 \(S\) 仅含小写字母(所以输入的 \(S'\) 也仅含小写字母)。

\(1\le n\le 10^4\)。

Solution

这种题一般是根据操作找性质题。我们可以先通过题目操作看出一些性质,把性质写下来。不放过题目每一个操作细节,再去考虑。

这个算法看起来非常迷惑,如果算法只有第一步,不排序,我们可以很容易解决问题。不难注意到 这 \(n\) 个字符串的最后一位,从第二个开始顺序取,最后取第一个,就是原字符串。这个结论也具有正确性。操作 \(i\) 把第 \(i-1\) 个元素扔到了最后面,操作 \(1\) 是原字符串,最后面本来就是最后一个元素,因此这么取是对的。

进一步的,我们发现,每一个字符串都能表示一个关系,具体的,形如 a***b 的字符串,表示 b 在 a 前面,且连续,题目给定了原字符串的最后一个字符,若我们已知这 \(n\) 个字符串的 第一位和最后一位,我们可从后往前推出所有的字符。

注意到每次操作实质是 将第一个字符移动到最后。未排序前最后一个字符串再次操作会回到第一个。因此,原串的 \(n\) 个字符中,每个字符都出现在字符串开头和末尾各一次。所以,把给定的末尾元素进行排序,就是与之相对应的第一个元素。这就构造出了 \(n\) 个形如 a***b 的字符串。

这么做看上去挺对,手玩样例也过了。但是,我们每次是从左边找右边。而右边找到多个。因此不一定直接接上就是对的。因为右边是无序的,随机接是没道理的。

仔细看排序规则,“若首字母相同,按照在 \(S\) 中的位置从小到大排序”。这是一个关键提示,它给定了 相等元素之间的先后顺序。因此,我们应当从尾元素找头元素。(也就是从右边找左边,因为左边是有序的。)从头元素找尾元素,是正着构造。那么从尾元找头元素应该是倒着构造。我们要先找到尾元素。这就用到了前面的性质。“第一个字符串,开头是头元素,尾是尾元素。”在此之前,我们应当先在左边找到头元素。左边的头元素,其所对应的就是尾元素。由于保证了 若相同按照在原串中的相对位置排序,故我们正着扫,扫到第一个和给定元素 \(p\) 相等的元素就是首元素。显然首元素对应的就是尾元素。前文提到,形如 a***b 的字符串描述了一个 a 在 b 后面(除了第一个字符串),我们已经把第一个字符串排除,因此可以忽略括号中的限制。

这个操作的本质是,对于每个头元素,都有唯一的尾元素与之衔接。我们在左边不断找倒数第一个,由倒数第二个可以推倒数第三个,以此类推……而且这样推结果答案是正确的。

接下来,我们在左边 倒着找尾元素。因为接下来我们要找到倒数第二个元素。以此类推,将最后的字符串倒序输出即可。这样操作,因为我们是在有序数组中找的,故顺序一定是对的。

参考代码
constexpr int N = 100010;
namespace solution
{
    int n,p;
    string s;
    string ans;
    char mp1[N],mp2[N];
    void solve()
    {
        cin>>n>>s>>p;
        string s1 = s;
        sort(s1.begin(),s1.end());
        for(int i=0;i<s1.size();i++)
            {if(s1[i] == s[p-1]) {p = i;break;}}
       // ans += s[p];
        for(int i=1;i<=s1.size();i++) 
        {
            ans += s[p];
            s1[p] = '*';
            for(int j=s.size()-1;j>=0;j--) if(s1[j] == s[p]){p = j;break;}
        }
        reverse(ans.begin(),ans.end());
        cout<<ans<<"\n";
    }
}

[NOIP2022] 种花

有一个 \(n\times m\) 的地图 \(S\),\(S_{i,j}\in\{0,1\}\) 表示该点可涂/不可涂。求在地图上涂 一个 字符 C 和字符 F 的方案数。

具体的,字符 C 要求 两个横直接至少空一行,不要求两条横一样长。字符 F 的上半部分要求和 C 一致,下面多出的部分长度至少为 \(1\)。

\(n,m\le 1000,1\le T\le 5\)。

Solution

又是计数题。

首先观察数据范围,加上 \(T\) 的限制后,\(n^3\) 大概是过不去的。我们尽量设计一种 \(n^2T\) 的算法。

注意到 F 就是比 C 长了一部分。我们先考虑 C 如何计数。

计数题的常见思考方式是,固定一个点 \(i\),考虑以 \(i\) 作为“端点“时的答案。本题中我们注意到 C 并不要求两条横一样长。如果我们固定 C 的左上端点,从这个点向下最多到 \(j\),那以该点作为左上端点的 C 下边所在行有 \(j-1\) 个。显然这牵扯到乘法原理。若第一行有 \(k\) 个点,它会产生 \(k-1\) 个贡献。与下面 \(j-1\) 行,每一行的贡献元素个数(即可访问实际元素个数 \(-1\))乘积之和即为以该点作为左上端点时的答案。

到这一步,对于任意 \((i,j)\),向右找到其贡献 \(x\)。再向下,对于每个合法的行,同样向右找其贡献 \(y\),产生 \(xy\) 的贡献。以此类推。这么做当然没问题。但是太慢了。显然不是 \(n^2\) 复杂度。

要想做到 \(n^2\) 复杂度,我们应当对于枚举到的每个左上端点 \((i,j)\),\(O(1)\) 计算其贡献。一种方式是 dp,还有一种方式是预处理。首先考虑优化掉每个左端点向右扫的过程。我们每次是找到右边第一个为 \(1\) 的点,这可以用后缀和维护。不难发现这个东西具有传递性。对于每一行,从右到左扫,若该点为 \(1\) 则赋为 \(-1\),否则累加答案。

接下来考虑优化掉向下找的过程。首先,类似的,我们可预处理出 \((i,j)\) 向下第一个为 \(1\) 的点。这也用后缀和维护,只不过倒过来。

进一步的,前面提到,我们预处理出每一行的贡献。再搭配上前面预处理的 每个点向下最多延伸距离,二者结合,可以预处理出 每个点向下的贡献和。这样每次就能线性求贡献了!(这里运用了乘法分配律。)

再考虑 F。

前文提到,F 和 C 的唯一区别在于下面多出了一部分 “竖”。其余部分都是和 C 一样的。因此,在预处理每个点向下的贡献和基础上,注意到每行 \(t\) 向下最远到 \(k\),会产生 \(k-t\) 的“竖”贡献。因此,转移的时候乘上 \(k-t\) 即可。(其实这也是乘法分配律。)

这样就是线性转移。

有一篇文章写的很好,挺形式化的,可以参考。Source。

参考代码
namespace solution
{
    int n,m,c,f;
    string s[N];
    int mp[N][N],r[N][N],h[N][N],g[N][N];
    void solve()
    {
        memset(mp,0,sizeof(mp));
        memset(h,0,sizeof(h));
        memset(g,0,sizeof(g));
        memset(r,-1,sizeof(r));
        cin>>n>>m>>c>>f;
        for(int i=1;i<=n;i++) cin>>s[i];
        for(int i=1;i<=n;i++)
            for(int j=1;j<=m;j++) mp[i][j] = (s[i][j-1]-'0');
        for(int i=1;i<=n;i++)
            for(int j=m;j;j--) r[i][j] = (mp[i][j]?-1:(r[i][j+1]+1)%mod);
        for(int i=n;i;i--)
            for(int j=1;j<=m;j++) g[i][j] = (mp[i][j]?0:(g[i+1][j]+r[i][j])%mod);
        for(int j=1;j<=m;j++)
        {
            for(int i=n,k=n;i;i--)
            {
                if(mp[i][j]) k = i-1;
                else h[i][j] = (h[i+1][j] + (r[i][j] *(k-i)))%mod; 
            }
        }
        int ans1 = 0,ans2 = 0;
        for(int i=1;i<=n;i++)
        {
            for(int j=1;j<=m;j++)
            {
                if(mp[i][j] || mp[i+1][j]) continue;
                ans1 += (r[i][j] *(g[i+2][j]))%mod;
                ans2 += (r[i][j] *(h[i+2][j]))%mod;
            }
        }
        cout<<(ans1*c)%mod<<" "<<(ans2*f)%mod<<"\n";
        return;
    }
    void mian()
    {
        int T,id;
        cin>>T>>id;
        while(T--) solve();
    }
}

[ABC374E] Sensor Optimization Dilemma 2

生产某种产品需要 \(N\) 个编号为 \(1,2,\dots,N\) 的工序。

每道工序 \(i\) 都有两种机器 \(S_i\) 和 \(T_i\) 可供购买使用。

  • 机器 \(S_i\) :每台每天可处理 \(A_i\) 个产品,每台成本为 \(P_i\) 日元。
  • 机器 \(T_i\) :每天可加工 \(B_i\) 件产品,单价为 \(Q_i\) 日元。

您可以购买任意数量的机器,也可以购买零台。

假设引入机器后,工序 \(i\) 每天可以处理 \(W_i\) 个产品。
在此,我们将生产能力定义为 \(W\) 的最小值,即 \(\min\limits_{i=1}^N W_i\)。

在总预算为 \(X\) 日元的情况下,求可实现的最大生产能力。

  • 所有输入值均为整数。
  • \(1 \le N \le 100\)
  • \(1 \le A_i,B_i \le 100\)
  • \(1 \le P_i,Q_i,X \le 10^7\)
Solution

注意到我们要求 \(\min\limits_{i=1}^N W_i\) 最大。这是最小值最大问题。不难想到二分。下面我们需考虑如何判定 \(mid\) 是否合法。

很自然想到,对于每道工序,在满足至少生产 \(mid\) 个的限制下,最小化代价。每个机器都有价格和生产个数。光取性价比最高的显然不行,因为 剩下的小部分可以拿性价比低,但是单价便宜的机器弥补。当然,也不一定是最后一个机器换用性价比低的。这样凭空想是没道理的。

我会暴力!暴力枚举第一个机器取的个数,由此推出第二个机器取的个数。枚举上界为 全部使用第一个机器要用多少。但这样有局限性,不能列举所有情况。我们再从第二个机器开始跑一遍。就能算出最小代价。肯定 TLE。

\(A_i,B_i\le 100\),这个数据很奇怪。尝试从这挖掘一些性质。注意到一台机器一个时刻最多生产 \(100\) 个零件。也就是说,我们最多购买 \(100\) 个性价比低的机器,就能赶上 \(1\) 个性价比高的机器。换言之,每最多购买 \(100\) 台性价比低的机器,就能换成一个性价比高的机器。

形式化的,定义 \(\dfrac{P_i}{A_i},\dfrac{Q_i}{B_i}\) 为性价比。满足 \(\dfrac{P_i}{A_i}>\dfrac{Q_i}{B_i}\) 当且仅当 \(P_i\times B_i>Q_i\times A_i\)。由此看来,购买 \(B_i\) 台 机器 \(1\) 和购买 \(A_i\) 台机器 \(2\) 制得的零件个数都是 \(A_i\times B_i\)。因此,每购买 \(A_i\) 件机器 \(2\),我们都可以换成 \(B_i\) 件机器 \(1\) 满足在不改变制作零件数前提下,代价更小。而 \(A_i,B_i\le 100\),故 性价比小的机器最多购买 \(100\) 台。

诈骗题。

参考代码
namespace solution
{
    int n,x;
    int a[N],p[N],b[N],q[N];
    bool check(int X)
    {
        int cost = 0;
        for(int i=1;i<=n;i++)
        {
            int minn = INF;
            for(int j=0;j<=10000 && j*a[i] <= X;j++) 
            {
                int t1 = ((X-(j*a[i]))*1.0/b[i])+((X - j * a[i]) % b[i] != 0);
                minn = min(minn,(int)t1*q[i]+j*p[i]);
            }
            for(int j=0;j<=10000 && j*b[i] <= X;j++)
            {
                int t1 = (X-(j*b[i]))/a[i]+((X - j * b[i]) % a[i] != 0);
                minn = min(minn,(int)t1*p[i]+j*q[i]);
            }
            cost += minn;
            if(cost > x) return 0;
        }
        return 1;
    }
    void solve()
    {
        cin>>n>>x;
        for(int i=1;i<=n;i++) cin>>a[i]>>p[i]>>b[i]>>q[i];
        int l = 1,r = 1e9; 
        while(l <= r)
        {
            int mid = (l+r) >> 1;
            if(check(mid)) l = mid+1;
            else r = mid - 1;
        }
        cout<<r<<"\n";
    }
}

P11187 配对序列

一个序列 \(s_1,\ldots s_{2k}\) 是配对的,当且仅当:

  • 对于任意 \(1\le i \le k\),\(s_{2i}=s_{2i-1}\)。
  • 对于任意 \(1\le i<k\),\(s_{2i}\ne s_{2i+1}\)。

注意,配对的序列长度必然为偶数。

例如,\(3,3,5,5,2,2\) 是配对的,而 \(2,2,2,2,5,5\)(\(s_2=s_3\) 不满足第二条要求)或者 \(1,2,3,3,1,1\)(\(s_1\ne s_2\) 不满足第一条要求)都不是配对的。

给出一个数列 \(a_1,\ldots, a_n\),求所有配对的子序列长度的最大值。

\(n\le 5\times 10^5,a_i\le 10^5\)。

Solution

Luogu CSP-J 2024 模拟赛 T4。比去年 CSP-J 简单不少。场上写了 \(n^2\) 暴力 dp。由于我已经 114514 年没写 dp 了。\(n^2\) dp 还写了很久。

首先,很难不注意到,一种序列合法,当且仅当形如 \(x,x,y,y,z,z\) 时。

我们要构造一个长这样子的最长的子序列。很难不想到 dp。这种转移也很显然,若当前数 \(x\) 要和一个值一样的拼接,则被拼接数不能再继续向前拼接。反之显然。这个结论也比较好注意到。

设计 \(f_{i,k}(k\in\{0,1\})\) 表示以 \(i\) 为结尾,\(a_i\) 是否和前面数拼接时的最长子序列。转移如下。

\(f_{i,0}=\max\limits_{j=1}^{i-1}\{f_{j,1}+1\}\)

\(f_{i,1}=\max\limits_{j=1}^{i-1}\{f_{j,0}+1\}\) 当且仅当 \(a_j=a_i\) 时才可转移。

我们又注意到,testcase 10 保证了每个数最多出现 \(1\) 次,显然此时答案为 \(0\),又混到 \(5\) pts。

所以写到这里,获得了 \(45+5=50\) 的分数。这一部分是非常简单的。

赛时 50 pts 代码
constexpr int N = 5000010;
namespace solution
{
    int n;
    int a[N];
    int f[N][3];
    bool flag = 1;
    int mp[N];
    void solve()
    {
        cin>>n;
        
        for(int i=1;i<=n;i++){ cin>>a[i];f[i][0] = f[i][1] = 1;if(mp[a[i]]){flag = 0;}mp[a[i]]=1;}
        if(flag)
        {
            cout<<"0"<<"\n";
            return;
        }
        int maxn = 0;
        for(int i=1;i<=n;i++)
        {
            for(int j=1;j<i;j++) 
            {
                if(a[j] == a[i]) f[i][1] = max(f[i][1],f[j][0]+1);
                else f[i][0] = max({f[i][0],f[j][1]+1});
                maxn = max({maxn,f[i][1]});
            }
        }
        if(maxn % 2 != 0) maxn --; // 处理一下答案为奇数时
        cout<<maxn<<endl;
        return;        
    }
}

下面考虑优化。

设计状态已经很简了。不好优化。考虑观察一下式子,看看式子到底在干什么。第一个式子其实就是求 \(\max\limits_{j=1}^{i-1} \{f_{j,1}\}\),这个前缀和扫一下就行。第二个式子求 \(i\) 前面和 \(a_i\) 相等的数中,最大的 \(f_{i,0}\),这个东西用一个桶维护即可。反正最大才 \(5\times 10^5\)。这两个式子优化方式都是经典的前缀扫描。对于相等元素的转移,开一个桶转移也是套路的。如果值域较大可能需要离散化,或者扔到 unordered_map 里。

但是这有问题。我们发现过不了样例。

此时应当回去看一下暴力转移,我们发现,只有 \(a_i\ne a_j\) 时,第二个转移方程才会进行。否则可能出现 111 这种子序列。也就是说,若当前 \(\max\limits_{j=1}^{i-1}\{f_{j,1}\}\) 满足 \(a_i=a_j\),是不能对 \(f_{i,0}\) 造成贡献的。

这种问题我们见多了,无论是树形 dp 转移,还是各种情况,见过不只一次。记录次大值转移即可。需要注意,次大值和最大值 \(a_i\) 不得相同,否则无意义。这个在传递次大值的时候判一下就行。

参考代码(赛后补题)
constexpr int N = 5000010;
namespace solution
{
    int n;
    int a[N];
    int f[N][3];
    bool flag = 1;
    int mp[N];
    int b[N];
    void solve()
    {
        f[0][0] = -1e9;
        cin>>n;
        for(int i=1;i<=n;i++){ cin>>a[i];f[i][0] = f[i][1] = 1;}       
        int maxn = 0,maxn1 = 0,maxx1=0,maxn2 =0,maxx2 = 0;
        for(int i=1;i<=n;i++)
        {
            f[i][1] = max(f[i][1],f[b[a[i]]][0]+1);
            maxn = max({maxn,f[i][1],f[i][0]});
            if(a[i]== a[maxx1])  
                {f[i][0] = max(f[i][0],f[maxx2][1]+1);}
            else f[i][0] = max(f[i][0],f[maxx1][1]+1);
            if(f[i][1] >= f[maxx1][1]) 
            {
                if(a[i] != a[maxx1]) // 向次大值传递时判一下,如果传递下去的,和新的最大值相同,那就不能传
                {
                    maxx2 = maxx1;
                }

                maxx1 = i;
            }
            else if(f[i][1] >= f[maxx2][1]) // 没更新最大值,那就更新次大值。
            {
                maxx2 = i;
            }
            b[a[i]] = i;
        }
        if(maxn % 2 != 0) maxn --;
        cout<<maxn<<endl;
        return;         
    }
}

P11188 「KDOI-10」商店砍价

Source

有一个正整数 \(n\),保证其只由数字 \(1\sim 9\) 构成。

你可以做任意多次如下操作:

  • 选择 \(n\) 的一个数位 \(x\),花费 \(v_x\) 的代价删除它,注意,此时 \(n\) 的数位个数会减少 \(1\),\(n\) 的值也会发生相应的变化;
  • 或者,花费 \(n\) 的代价把剩余的所有数位删除。

求把整个数删除的最小代价。

  • \(1\le t\le 10\);
  • \(1\le n< 10^{10^5}\);
  • 对于任意 \(1\le i\le 9\),\(1\le v_i\le 10^5\);
  • \(n\) 由数字 \(1\sim 9\) 构成。
Solution

题是好题。但是放在 CSP-S 模拟赛 T1 我是不认可的。没切。但是感觉挺 edu 的。

注意到这个数最多有 \(10^5\) 位,得当作 string 处理。

首先考虑暴力怎么做。如果我们令 \(x,y\) 都要被单个删,那么删除的顺序是没有影响的。这个结论也比较好证,无论先删 \(x\) 还是先删 \(y\),最后剩下的数形态是一样的。即每个数要么被单个删,要么被整体删。dfs 即可。

进一步的,既然我们的目的是删除所有数,且代价最小。不妨贪心地考虑,我们整体删一个数,当且仅当将所有数位,单个删数的代价大于整体删除的代价。我们发现 \(v_i\le 10^5\),这个代价也不是很大。注意到如果当前还有 \(x\) 位数,整体删除至少需要 \(10^{x-1}\) 代价。而单个删除最多只需要 \(10^5\times x\) 代价。当 \(x\ge 6\) 时,\(10^{x-1}>10^5\times x\) 恒成立。因此,当还剩下的位数多于 \(6\) 位时,一定是单个删更优。所以最多保留 \(5\) 位。这种题你注意到答案上界很小,就可以往这方面考虑了。

然后你可以枚举保留哪五位进行暴力。复杂度肯定比直接暴力优的。

这种 dfs 一般可以转化成 dp。前面提到,我们令一个数位被单个删除,被删除的顺序是无影响的。但是最后剩下的数,相对顺序是不可改变的。因此,考虑设计状态,令 \(f_{i,j}\) 表示从右往左扫到前 \(i\) 个数,保留 \(j\) 个数最后整体删的最小代价。根据前面的结论,\(j\in[0,5]\)。所以完全存的下。

\[f_{i,j}=\min(f_{i-1,j}+v_i,f_{i-1,j-1}+n_i\times 10^{j-1}) \]

注意到 \(f_i\) 的转移依赖于 \(f_{i-1}\),所以这一维滚动数组可以直接压掉。需要注意,循环顺序别弄反了。

好题。

参考代码
constexpr int N = 500010;
namespace solution
{
    string n;
    int val[N];
    int power[N];
    size_t f[N][10];
    void solve()
    {
        memset(f,0x3f,sizeof(f));
        cin>>n;
        int len = n.size();
        n = ' '+ n;
        for(int i=1;i<=9;i++) cin>>val[i];
        f[len][0] = val[n[len]-'0'];
        f[len][1] =  n[len]-'0';
        for(int i=len-1;i>=1;i--)
        {
            for(int j=0;j<=5;j++)
            {
                f[i][j] = min(f[i][j],f[i+1][j]+val[n[i]-'0']);
                if(j) f[i][j] = min(f[i][j],f[i+1][j-1]+(n[i]-'0')*power[j]);
            }
        }
        size_t ans = 1e11;
        for(int i=0;i<=5;i++) ans = min(ans,f[1][i]);
        cout<<ans<<"\n";
        return;
    }
    void mian()
    {
        power[1] = 1;
        for(int i=2;i<=6;i++) power[i] = power[i-1]*10;
        int T,c;
        cin>>c>>T;
        while(T--) solve();
    }
}

P1685 游览

给定一个 DAG。不保证无重边。求把所有从 \(s\) 到 \(t\) 的路径走完,所走的边权值是多少。若两次路径经过边权相同,但是边不同我们认为路径是不相同的。

\(2\le n\le 10^4,1\le m\le 5\times 10^4\)。

Solution

图上计数题。一般用 dp 求解,加法原理,乘法原理也都是常用的。

考虑拆分贡献,计算每条边的贡献。考虑对于边 \(u\rightarrow v\),走到 \(u\) 有 \(k\) 种情况,则一定会走 \(k\) 该边。产生 \(k\times dis_{u,v}\) 的贡献。

至于 \(k\) 如何计算,这是典型的加法原理。转移即可。

如何计算走到 \(v\) 的贡献和呢?这个也是加法原理。

然后你想直接转移,发现假了。原因在于算 \(v\) 的时候,可能 \(u\) 还没转移。这就导致转移不完全。问题出在转移顺序上。

这很简单,原题保证了是个 DAG。可以直接拓扑排序。当一个点的入度为 \(0\) 的时候是可以放心转移的,这也就意味着,他的“父亲”全部转移完了。DAG 上 dp 一般是按拓扑序跑。哪怕不是 DAG,tarjan 缩点后跑也是按拓扑序跑。

对了,根据题意,我们是不需判重边的。

参考代码
constexpr int N = 2000010;
constexpr int MOD = 10000;
namespace solution
{   
    #define int long long
    #define x first
    #define y second
    int n,m,s,t,t0;
    typedef pair<int,int> PII;
    vector <PII> Edge[N];
    queue <int> q;
    __int128_t f[N][3];
    int in[N];
    bool check_edge(int u,int v,int w)
    {
        for(auto t:Edge[u])
            if(t.x == v && t.y == w) return 0;
        return 1;
    }
    void tuopusort()
    {
        f[s][0] = 1;
        q.push(s);
        while(!q.empty())
        {
            int u = q.front();
            q.pop();
            for(auto t:Edge[u])
            {
                int v = t.x,w = t.y;
                f[v][0] = (f[v][0]+f[u][0])%MOD;
                f[v][1] = (f[v][1]+ (w* f[u][0])%MOD + f[u][1])%MOD;
                in[v] --;
                if(!in[v]) q.push(v);
            }
        } 
    }
    void solve()
    {
        cin>>n>>m>>s>>t>>t0;
        for(int i=1;i<=m;i++)
        {
            int u,v,w;
            cin>>u>>v>>w;
            if(u == v) continue;
            Edge[u].push_back(PII(v,w));in[v] ++;
        }
        tuopusort();
        cout<<(long long)((f[t][1] + (f[t][0]-1)*t0)%MOD + MOD) % MOD <<"\n";
    }
}

P5603 小 C 与桌游

有一个 \(n\) 个点,\(m\) 条边的 DAG。(不保证连通)一个点能走 当且仅当能走到该点的点都被走。当走到点 \(u\) 时,若 \(u\) 比走过的任何一个点编号都大,对答案造成 \(1\) 的贡献。求最小和最大贡献。

\(1\le n,m\le 5\times 10^5\)。

Solution

注意到“一个点能走 当且仅当能走到该点的点都被走”,即必须按照拓扑序走。这是一个拓扑排序问题。

首先考虑最大值。只需要将队列中元素升序排序即可。也就是开个 priority_queue。每次贪心取最小的点进行扩展。这样可以保证不会落下编号较大的点对较小的点的贡献。证明考虑交换法。若我们先选编号较大的进行转移,则后面的点想要产生贡献的 编号下界就会变大。这就导致答案不可能更优。

考虑最小值怎么做。一个直观的想法是类比算最大值。把 priority_queue 改成降序排序,也就是每次选择当前编号最大的转移。但这样选择的较大点可能是冗余的。求贡献最大值可以直接从小到大贪,是因为我们期望贡献最大,不希望看到在满足拓扑序的前提下,编号大的节点比编号小的节点提前访问。但现在让编号大的先转移,只是期望他能对后面的数造成贡献。很遗憾并不是所有时候都能造成贡献,且很可能先拿一个比他大的点,再拿他是更优的。

因为我们要让代价最小,故若当前 queue 里的数有小于 \(maxn\) 的,肯定先拿。因为拿它不会产生代价。至于剩下的,那就按编号从大到小拿。

参考代码
constexpr int N = 1000010;
namespace solution
{
    int n,m,maxn = 0;
    vector <int> Edge[N];
    int in[N],in1[N];
    int res1 = 0,res2 = 0;
    void solve1()
    {
        priority_queue<int,vector <int>,greater<int> > q;
        for(int i=1;i<=n;i++) if(!in[i]) q.push(i);
        while(!q.empty())
        {
            int u = q.top();
            q.pop();
            if(u > maxn) {maxn = u,res1 ++;}
            for(auto v:Edge[u])
            {
                in[v] --;
                if(!in[v]) q.push(v);
            }
        }
        cout<<res1<<"\n";
        return;
    }
    void solve2()
    {
        maxn = 0;
        priority_queue <int> q1;
        queue <int> q2;
        for(int i=1;i<=n;i++) if(!in1[i]) q1.push(i);
        while(!q1.empty())
        {
            if(q1.top() > maxn) res2 ++;
            while(!q1.empty()) {q2.push(q1.top());q1.pop();}
            bool flag = 0;
            while(!q2.empty())
            {
                int u = q2.front();
                q2.pop();
                maxn = max(maxn,u);
                for(auto v:Edge[u])
                {
                    in1[v] --;
                    if(!in1[v]) 
                    {
                        if(v > maxn) q1.push(v);
                        else q2.push(v);
                    }
                }
            }
        }
        cout<<res2<<"\n";
    }
    void solve()
    {
        cin>>n>>m;
        for(int i=1;i<=m;i++)
        {
            int u,v;
            cin>>u>>v;
            Edge[u].push_back(v);
            in[v] ++,in1[v] ++;
        } 
        solve1();
        solve2();
        return;
    }
}

P10166 [DTCPC 2024] 环

有一个 \(n\) 个点,\(m\) 条边的有向图。这 \(m\) 条边权值为 \(0\)。除此之外,你还可以任意添加边。规定添加一条 \(u\rightarrow v\) 的有向边权值为 \(a_u+a_v\)。求若干次操作后,最小环的大小。(不包括自环。)

\(2\le n\le 5\times 10^5,n-1\le m\le 10^6,1\le a_i\le 10^9\)。

Solution

首先,注意到若原图中有环,则答案必定为 \(0\)。我们只需考虑原图中没有环的情况。也就是考虑 DAG 怎么做。

下文定义若干点“在一条链上”,当且仅当点集中只存在一个入度为 \(0\) 的点,一个出度为 \(0\) 的点。其余点入度出度均为 \(1\)。

Claim:通过添加边构造出来的环,最优的一定“在一条链上”。

Proof:
img

如图,显然最优答案不可能是连 \(2,5\),再连接 \(1,2\)。因为保证点权必为正整数。

这就好做了。在 DAG 上跑拓扑排序,同时转移得出每个点前面最小的数,尝试连接,最后对所有点的答案取 \(\min\) 即可。

前文提及,这是 通过添加边构造环 情况,即利用原本边权为 \(0\) 的边。还可以直接构造。直接构造的最小值显然。

本题注意考虑全面,以及 二元环通常固定一个点,考虑另一个点。不要整体扫链。应该从每个点的角度考虑问题。

参考代码
constexpr int N = 5000010;
constexpr int INF = 0x3f3f3f3f;
namespace solution
{
    int n,m;
    int val[N],a[N];
    vector <int> Edge[N];
    int in[N];
    queue <int> q;
    int f[N];
    int cnt = 0;
    void tuopusort()
    {
        while(!q.empty())
        {
            int u = q.front();
            cnt ++;
            q.pop();
            for(auto v:Edge[u]) 
            {
                in[v] --;
                f[v] = min({f[v],f[u],val[u]});
                if(!in[v]) q.push(v);
            }
        }
    }
    void solve()
    {
        cin>>n>>m;
        for(int i=1;i<=n;i++) 
        {
            cin>>val[i];
            a[i] = val[i];
        }
        memset(f,INF,sizeof(f));
        for(int i=1;i<=m;i++)
        {
            int u,v;
            cin>>u>>v;
            Edge[u].push_back(v);
            in[v] ++;
        }
        for(int i=1;i<=n;i++) if(!in[i]) q.push(i);
        tuopusort();
        if(cnt < n) {cout<<"0\n";exit(0);}
        sort(a+1,a+n+1);
        int minn = 1e14;
        for(int i=1;i<=n;i++) minn = min({minn,val[i]+f[i],2*(a[1]+a[2])});
        if(minn == INF) cout<<"0\n";
        else cout<<minn<<"\n";
        return;
    }
}

P11186 三目运算

计算一个三目运算表达式。只有比大小和赋值。表达式可以嵌套。

每个测试点给定一个三目运算表达式,未知数为 \(x\)。有 \(q\) 次询问,每次询问给定 \(a\),求当 \(x=a\) 时,表达式的结果。

\(n\le 10^5,1\le m\le 10^5,1\le q \le 10^5\)。

Solution

好题。加深了对递归的理解。

首先,注意到表达式可以嵌套,大概率要递归做。把一个大字符串分解为一个一个小字符串处理。考虑如何分解一个一个子任务。

所有的三目运算,都形如 x>q?A:B。其中,\(q\) 为正整数,\(A,B\) 可以是子表达式,也可为正整数。我们先考虑不嵌套该怎么做,即当 \(s_i='x'\) 时。很简单,首先判断符号,即判断 \(s_{i+1}\) 为大于号还是小于号,提取数字 \(q\)。再检查 \(s_{i+2}\sim s_{i+k},s_{i+k+1}\sim s_{j}\) 这两个数。最后在根据前面检查决定执行赋值为 A 还是 B。

当出现嵌套时,每个三目表达式仍是一个子表达式。考虑递归。当表达式成立时是平凡的,向左递归即可。当表达式不成立时,我们需要跳过整个左表达式,即需得知左表达式的终止位置。这还得递归把整个左表达式扫一遍。回溯的时候不断 pushup 右表达式的 end address。然后你可以顺便把答案给 pushup 了。

这就得到了本题的暴力做法,每次先递归左表达式,右表达式,回溯的时候更新答案。这样要对每个 \(x\) 跑一遍,肯定过不了。

进一步的,我们回溯的时候是判定该点是否在表达式区间内,不难想到,所有在子表达式区间内的数都执行该子表达式。所以我们递归的时候,记录 \(l,r\) 表示当当前区间,递归到叶子的时候直接赋值即可。这样每次询问就是 \(O(1)\) 的。

具体的,每次 check 当前是小于号还是大于号。每次先递归左表达式,再根据左表达式的最后一个元素位置,递归右表达式。到达叶子直接赋值。有点分治的思想,不断拆分区间。

参考代码
constexpr int N = 5000010;
namespace solution
{
    int m,q;
    string s;
    int ans[N];
    int get_num(int pos,int &num)
    {
        while(isdigit(s[pos]))
        {
            num = num * 10 + (s[pos] - '0');
            pos ++;
        }
        return pos;
    }
    int calc(int l,int r,int pos)
    {
        if(isdigit(s[pos])) 
        {
            int number = 0;
            int p = get_num(pos,number);
            for(int i=l;i<=r;i++) ans[i] = number;
            return p;
        }
        int number=0;
        int len = get_num(pos+2,number);
        if(s[pos+1] == '>') 
        {
            int ed = calc(number+1,r,len+1); // 先左再右,根据左边的返回值做右边
            return calc(l,number,ed+1); 
        }
        else
        {
            int ed = calc(l,number-1,len+1);  
            return calc(number,r,ed+1); 
        }
    }
    void solve()
    {
        cin>>m>>q>>s;
        calc(1,m+1,0);
        while(q--)
        {
            int x;
            cin>>x;
            cout<<ans[min(x,m+1)]<<"\n";
        }
    }
}

[ABC376E] Max × Sum

给你长度为 \(N\) 的序列: \(A = (A_1, A_2, \dots, A_N)\) 和 \(B = (B _; 1, B _; 2, \dots, B _; N)\) .
设 \(S\) 是大小为 \(K\) 的 \(\lbrace1, 2, \dots, N\rbrace\) 的子集。求以下表达式的最小可能值:

\(\displaystyle \left(\max\limits_{i \in S} A_i\right) \times \left(\sum\limits_{i \in S} B_i\right)\)

给你 \(T\) 个测试用例,请逐个求解。

\(1\le N,T\le 2\times 10^5\)。

Solution

好牛的题目,自己没想到。

注意到 \(A,B\) 是捆绑的。\(A\) 是最大值,\(B\) 是求和。由于选择的是子集,和顺序是没有关系的。注意到数据范围是 \(2\times 10^5\),典型的 \(n\log\) 复杂度。考虑将 \(A,B\) 按照 \(A\) 从小到大进行结构体排序。这样,对于 \(\forall A_i\),都是 \([1,i]\) 的最大值。

考虑 \(B\) 的选择,显然,\(B_i\) 是一定要选的。因为 \(A_i\) 是最大值。然后你除了 \(B_i\),前面还要选 \(k-1\) 个。我们期望选择的总和最小。一种暴力的思路是每次对前面的进行排序,顺序取 \(k-1\) 个。其实直接拿堆做就完了。每移动端点,把一个新的数扔到堆里,此时会有 \(k\) 个数,弹出最大的即可。

参考代码
constexpr int N = 1000010;
constexpr int INF = 9e18;
namespace solution
{
    struct Node
    {
        int a,b;
        bool operator <(const Node& A)const{
            return a < A.a;
        }
    }num[N];
    int n,k;
    void solve()
    {
        priority_queue <int> q;   
        cin>>n>>k;
        for(int i=1;i<=n;i++) cin>>num[i].a;
        for(int i=1;i<=n;i++) cin>>num[i].b;
        sort(num+1,num+n+1);
        int sum = 0;
        for(int i=1;i<k;i++)
        {
            sum += num[i].b;
            q.push(num[i].b);
        }
        int minn = INF;
        for(int i=k;i<=n;i++)
        {
            sum += num[i].b;
            minn = min(minn,sum * num[i].a);
            q.push(num[i].b);
            sum -= q.top();
            q.pop();
        }
        cout<<minn<<"\n";
    }
    void mian()
    {
        int T;
        cin>>T;
        while(T--) solve();
    }
}

xyd CSP-S 2024 模拟 T1 confess

给定一个长度为 \(n\) 的字符串 \(s\),初始化 \(sum=0\),你需要从左到右扫一遍 \(s\),若 \(s_i\) 为 +,令 \(sum+1\),若 \(s_i\) 为 -,令 \(sum-1\)。你可以进行 \(m\) 次操作,将原本的操作变为 \(sum\operatorname{xor}1\)。

请对 \(m=0,1,\dots n\) 分别计算答案。

\(n\le 3\times 10^5\)。

Solution

小清新签到题。场切之。

首先考虑 \(x\operatorname{xor}1\) 的操作实质。显然,当 \(x\bmod 2\ne 1\) 时,等价于 \(x+1\),反之等价于 \(x-1\)。

\(n^2\) 的 dp 是容易的。\(f_{i,j}\) 表示前 \(i\) 个数,进行 \(j\) 次异或操作时的最大值。\(f_{i,j}=\max(f_{i-1,j}+a_i,f_{i-1,j-1}\operatorname{xor}1),a_i\in\{1,-1\}\)。

我们刚开始就注意到了异或 \(1\) 的实质。不妨贪心的考虑,我们使用异或操作,当且仅当 \(ans\bmod 2=0\) 且当前操作为 \(-1\)。记该情况为 \(p\)。仔细考虑,我们要在 \(n\log\) 的复杂度内计算 \(m\in[0,n]\) 的所有情况,肯定有一部分 \(m\) 是不能全部将 \(p\) 情况时都修改为 \(\operatorname{xor}1\)。但每次修改,都使原本的答案 \(+2\)。

因此,先线性扫一遍,计算如果不进行任何修改操作的答案,记作 \(ans_1\)。再线性扫一遍,计算 如果满足 \(p\) 情况的所有值都修改的答案,记作 \(ans_2\)。同时统计此时使用的修改次数 \(k\)。显然,\(m\in[1,k]\) 的情况可通过 \(ans_1\) 不断 \(+2\) 来计算。

至于 \(k\) 以后的 \(m\),再使用异或操作不可能使答案更优,我们肯定先确保答案不劣。故先把异或操作安到 如果将该操作修改为异或,操作不变情况上。此时操作应基于 将所有修改为异或操作,能使答案更优。记录个数为 \(q\),从 \(k+1\) 到 \(q\) 的答案显然和 \(k\) 的答案一致。

如果继续使用异或操作,答案只会更劣,具体的,在原来基础上 \(-2\)。

参考代码
namespace solution
{
    void solve()
    {
        int tot = 0;
        string s;
        cin>>s;
        int ans1 = 0,ans2 = 0,k = 0;
        for(int i=0;i<s.size();i++) 
        {
            if(s[i] == '+') ans1 ++;
            else ans1 --;
        }
        for(int i=0;i<s.size();i++)
        {
            if(s[i] == '+') ans2 ++;
            else
            {
                if(ans2 % 2 == 0) 
                {
                    s[i] = '*'; //标记该操作被修改为异或
                    k ++;
                    ans2 ++;
                }
                else ans2 --;
            }
        }
        for(int i=0;i<=k;i++) {cout<<ans1 + 2*i<<" ";tot ++;}
        int cnt = 0,ans3 = 0;
        for(int i=0;i<s.size();i++)
        {
            if(s[i] == '+' && ans3 %2 == 0) cnt ++; //如果本来就是加,且没有被修改为异或,且答案不变
            if(s[i] == '-') cnt ++;
            if(s[i] == '+' || s[i] == '*') ans3 ++; //统计答案,被修改为加的也统计上
            else ans3 --;
        }
        for(int i=1;i<=cnt;i++){ cout<<ans2<<" ";tot ++;}
        int res = 1;
        for(int i=1;i<=s.size()-tot+1;i++){cout<<ans2-2*res<<" ";res ++;}
        cout<<"\n";
        return; 
    }
    void mian()
    {
        int T;
        cin>>T;
        while(T--) solve();
    }
}

P4503 [CTSC2014] 企鹅 QQ

Source

有 \(n\) 个等长的字符串,两个字符串是 ”相似的“ 当且仅当有恰好有一位是不同的。如 Penguin1 和 Penguin2 是 “相似的”,但 Penghin1 和 1Penguin 不是 “相似的”。即删除 相同位置的字母后两字符串相同。

保证不存在两个相同的字符串。

求有多少对 “相似” 的字符串。

\(n\le 3\times 10^4,|S|\le 200\)。最多有 \(64\) 个字符串。

Solution

首先搞明白定义,两个字符串 “相似” 当且仅当删除相同位置的一个字母后一样。

然后你注意到 \(|S|\) 很小,只有 \(200\),不妨枚举删除哪个字符。对于 \(\forall i\in[1,|S|]\),比对所有字符串删除该字符后的形态。具体的,若有 \(k\) 个字符串删除该字符的形态相同,产生 \(\dfrac{k\times (k-1)}{2}\) 的贡献。由于 ”相似的“ 字符串对的定义,且保证了两个字符串不可能完全一样,故一对相似的字符串不可能被重复统计。

又到了喜闻乐见的字符串比较问题。可以用字符串 hash 解决问题。每个字符串除去第 \(i\) 个字符后的 hash 值的计算也是容易的。举个例子即可。

如何比较有多少个字符串在去除第 \(i\) 个字符后的 hash 值相等呢?一个比较直观的方法是对 hash 值排序,然后线性扫一遍即可。

需要注意的是,尽量不要对 hash 值作除法,会掉精度,就不对了。可以作乘法。

参考代码
namespace solution
{
    int n,l,s;
    char str[N][500];
    size_t hash[N][400];
    size_t h[N];
    map <size_t,size_t> mp;
    set <size_t> st;
    void init()
    {
        for(int i=1;i<=n;i++)
        {
            for(int j=1;j<=l;j++) hash[i][j] = hash[i][j-1] * p + str[i][j];
        }
        h[0] = 1;
        for(int i=1;i<=l;i++) h[i] = h[i-1] * p;
    }
    size_t get_hash(int i,int l,int r){return hash[i][r] - hash[i][l-1]*h[r-l+1];}
    void solve()
    {
        cin>>n>>l>>s;
        for(int i=1;i<=n;i++)
            for(int j=1;j<=l;j++) cin>>str[i][j];
        init();
        vector <size_t> qwq;
        int ans = 0;
        for(int i=1;i<=l;i++)
        {
            qwq.clear();
            for(int j=1;j<=n;j++)
            {
                size_t Hash = get_hash(j,1,i-1)*h[l-i] + get_hash(j,i+1,l);
                qwq.push_back(Hash);
            }
            sort(qwq.begin(),qwq.end());
            int tot = 1;
            for(int j=0;j<qwq.size()-1;j++)
            {
                if(qwq[j] == qwq[j+1])
                {
                    ans += tot;
                    tot ++;
                }
                else tot = 1;
            }
        }
        cout<<ans<<endl;
    }
}

P7469 [NOI Online 2021 提高组] 积木小赛

Source

有两个长度为 \(n\) 的字符串。第一个串可以任意删字符,第二个串必须删除 左边连续或者右边连续的字符。例如,abcdef 可以变成 cd,但不能变成 abef。

两种情况不同,当且仅当`````

求有多少种删除字符的情况,使得两个字符是相同的。

\(n\le 3000\)。

Solution

题目给定了两个串,第一个串可以随便删,保留的任意子序列都是合法的。第二个串保留的必须是一段连续的字串。显然第二个串的限制更大。我们肯定去枚举限制大的,来匹配小的。\(n\le 3000\),\(n^2\) 枚举是可过的。所以我们 \(n^2\) 枚举第二个串。

注意理解题意:两种情况不同当且仅当 剩下的字母不同。因此,\(s_2\) 的每个子串最多产生一次贡献。,这个性质非常关键。设枚举区间为 \([i,pos]\),从 \([i,pos]\) 到 \([i,pos+1]\) 仅 \(pos\) 不同。所以我们从小到大贪心匹配 \(s_1\) 即可。

具体的,设当前匹配 \(s_1\) 匹配到 \(p\),\(p\) 的移动是满足单调性的。随着 \(pos\) 的右移,\(p\) 不可能回退。还需要注意的是,由于 \(s_2\) 的每个 不同的子串最多产生一次贡献,若 \(s_2\) 的一个子串可匹配,我们把其扔到 set 里面,因为多个相同的子串只能产生一次贡献。

直接扔到 set 里会被卡常,最好手写去重。

复杂度是 \(n^2\) 级别的。

参考代码
namespace solution
{
    int len;
    string s1,s2;
    unordered_set <size_t> st;
    size_t qwq[N];
    int cnt = 0;
    void solve()
    {
        cin>>len;
        cin>>s1>>s2;
        s1 = ' '+ s1,s2 = ' ' + s2;
        for(int i=1;i<=len;i++)
        {
            string t;
            int tot = 1;
            size_t hash = 0;
            for(int j=i;j<=len;j++) 
            {
                hash = hash * p + s2[j];
                while(tot <= len && s1[tot] != s2[j]) tot ++;
                if(tot > len) break;
                tot ++;
                qwq[++cnt] = hash;
            }
        }
        sort(qwq+1,qwq+cnt+1);
        cout<<unique(qwq+1,qwq+cnt+1)-qwq-1<<"\n";
    }
}

P3538 [POI2012] OKR-A Horrible Poem

Source

给定一个由小写英文字母组成的字符串 \(S\),共 \(q\) 次询问,每次询问给定两个值 \(l,r\),求 \(s_l~s_r\) 字符串的最短循环节。

\(n\le 5\times 10^5,q\le 2\times 10^6\)。

Solution

这是经典问题。求字符串的最短循环节常用 hash 解决。

解决本问题需要知道如下结论。

  • 快速判定 \(len\) 是否为字符串 \(s\) 的最短循环节,只需判定 \(s_{l}~s_{r-len},s_{l+len},s_r\) 是否相同即可。

  • 最短循环节 \(len\) 必定为字符串长度的因数。即 \(|s| | len\)。

然后就能直接做了。首先将字符串 \(s\) 进行哈希,然后预处理每个数的因数。

vector <int> qwq[N];
for(int i=1;i<=length;i++)
{
    for(int j=i;j<=length;j+=i) qwq[j].push_back(i);
}

暴力判断即可。可能被卡常。但能应对绝大多数情形。

参考代码
namespace solution 
{   
    template<typename T> inline void read(T &x) {
	x = 0; T k = 1; char in = getchar();
	while (!isdigit(in)) { if (in == '-') k = -1; in = getchar(); }
	while (isdigit(in)) x = x * 10 + in - '0', in = getchar();
	x *= k;
    }
    inline string read1()
{
    char ch = getchar();
    string st1 = "";
    while (!((ch >= 'a') && (ch <= 'z')))
        ch = getchar();
    while ((ch >= 'a') && (ch <= 'z'))
        st1 += ch, ch = getchar();
    return st1;
}
    int len;
    size_t hash[N],h[N];
    bool not_prime[N];
    vector <int> pri;
    int g[N];
    string s;
    vector <int> qwq[N];
    void init()
    {
        for(int i=1;i<=len;i++) hash[i] = hash[i-1] * p + s[i];
        h[0] = 1;
        for(int i=1;i<=len;i++) h[i] = h[i-1] * p;
    }
    size_t get_hash(int l,int r){return hash[r] - hash[l-1] * h[r-l+1];}
    void solve()
    {
        read(len);
        s = read1();
        s = ' ' + s;
        init(); 
        for(int i=1;i<=len;i++)
            for(int j=i;j<=len;j+=i) qwq[j].push_back(i);
        int q;
        read(q);
        while(q--)
        {
            int l,r;
            read(l);read(r);
            if(get_hash(l+1,r) == get_hash(l,r-1)){cout<<"1\n";continue;}
            int length,ans;
            length = ans = r-l+1;
            bool get_ans = 0;
            for(auto j:qwq[length])
            {
                if(get_hash(l,r-j) == get_hash(l+j,r)) {get_ans = 1;cout<<j<<"\n";break;}
            }
        }
    }
}

P1311 [NOIP2011] 选择客栈

有一个长度为 \(n\) 的序列,序列上每个元素都有两个值 \(a_i,p_i\)。求有多少个点对 \((i,j)\),满足 \(a_i=a_j\) 且 \(\min\limits_{k=i}^j\{p_k\}\le p\)。

\(2\le n\le 2\times 10^5\),\(1\le a_i\le 50\)。

Solution

经典点对计数题。常见的思路是固定一个端点,用数据结构维护另一个端点。这样复杂度一般能降到 \(n\log\) 及以下。当然也不一定用数据结构,反正一定要固定一个端点,常见的是固定右端点,因为你一般从左往右扫序列,扫右端点的同时顺便把左端点信息维护了。

对于本题,有个很重要的性质,当固定右端点 \(i\) 时,若 \((i,j)\) 合法,\(a_k=a_i\) 且 \(k<j\),则 \((i,k)\) 必合法。这也很显然,大区间包含小区间,题目只要求区间内存在一个数 \(q\),满足 \(b_q\le p\) 即可。

因此,以 \(i\) 为右端点,合法的区间数量为 \(i\) 向左第一个 \(b_k \le p\) 的位置左边 \(a_k=a_i\) 的值的数量。

然后可以模拟,从左往右扫序列,\(k\) 可以实时维护。然后每个值左边每个 \(a_i\) 的数量也可以递推。复杂度 \(O(nk)\),足够通过本题。

参考代码
namespace solution
{
    int color[N][50];
    int n,k,p,lst;
    void solve()
    {
        cin>>n>>k>>p;
        int ans = 0;
        for(int i=1;i<=n;i++)
        {
            int a,b;
            cin>>a>>b;
            for(int j=0;j<k;j++) color[i][j] = color[i-1][j];
            if(b <= p) lst = i;
            ans += color[lst][a];
            color[i][a] ++;
        }
        cout<<ans<<"\n";
    }
}

P1315 [NOIP2011 提高组] 观光公交

风景迷人的小城 Y 市,拥有 \(n\) 个美丽的景点。由于慕名而来的游客越来越多,Y 市特意安排了一辆观光公交车,为游客提供更便捷的交通服务。观光公交车在第 \(0\) 分钟出现在 \(1\) 号景点,随后依次前往 \(2,3,4,\cdots,n\) 号景点。从第 \(i\) 号景点开到第 \(i+1\) 号景点需要 \(D_i\) 分钟。任意时刻,公交车只能往前开,或在景点处等待。

设共有 \(m\) 个游客,每位游客需要乘车 \(1\) 次从一个景点到达另一个景点,第 \(i\) 位游客在 \(T_i\) 分钟来到景点 \(A_i\),希望乘车前往景点 \(B_i\)(\(A_i<B_i\))。为了使所有乘客都能顺利到达目的地,公交车在每站都必须等待需要从该景点出发的所有乘客都上车后才能出发开往下一景点。

假设乘客上下车不需要时间。一个乘客的旅行时间,等于他到达目的地的时刻减去他来到出发地的时刻。因为只有一辆观光车,有时候还要停下来等其他乘客,乘客们纷纷抱怨旅行时间太长了。于是聪明的司机 ZZ 给公交车安装了 \(k\) 个氮气加速器,每使用一个加速器,可以使其中一个 \(D_i-1\)。对于同一个 \(D_i\) 可以重复使用加速器,但是必须保证使用后 \(D_i\ge0\)。

那么 ZZ 该如何安排使用加速器,才能使所有乘客的旅行时间总和最小?

对于 \(10\%\) 的数据,\(k=0\)。

对于 \(20\%\) 的数据,\(k=1\)。

对于 \(40\%\) 的数据,\(2 \le n \le 50\),\(1 \le m \le 10^3\),\(0 \le k \le 20\),\(0 \le D_i \le 10\),\(0 \le T_i \le 500\)。

对于 \(60\%\) 的数据,\(1 \le n \le 100\),\(1 \le m \le 10^3\),\(0 \le k \le 100\),\(0 \le D_i \le 100\),\(0 \le T_i \le 10^4\)。

对于 \(100\%\) 的数据,\(1 \le n \le 10^3\),\(1 \le m \le 10^4\),\(0 \le k \le 10^5\),\(0 \le D_i \le 100\),\(0 \le T_i \le 10^5\)。

Solution

前 \(10\%\) 模拟即可。模拟车辆运行过程,不难发现,车辆到站时,是否需要等,需要等多长时间,只取决于最晚到站的人。当前时间和最晚到站人的到站时间取 \(\max\) 即可。

考虑当 \(k=1\) 怎么做。我们只能在一段区间安放加速器,肯定贪心地选择。具体的,枚举每一段路程 \(i\rightarrow i+1\),考虑给这一段路程安上加速器造成什么影响。显然在 \(i\) 下车的人行驶时间都会 \(-1\)。到站时间也会 \(-1\)。若到站时间 \(<\) 最晚到站的人时间,则车辆必须等到最晚到站人的时间才能继续开,也就不可能对后续车辆运行造成影响,退出即可。否则至少会让后面一站的人减少 \(1\) 的行程时间。

如此,我们可算出 在每个区间安装一个加速器,会减少多少时间。取 \(\max\) 即可。由于 \(k=1\),不需要对到站时间进行实质上的修改,只需要在原答案基础上减去即可。

再考虑 \(k>1\) 时怎么做。

同样是贪心,执行 \(k\) 次上述 Greedy select。只不过每轮 Greedy select 后需要对到站时间进行修改。修改的时候类比贪心过程,若当前到站时间小于等于最晚到站人的时间,就不会对后续答案造成影响。退出即可。

复杂度最坏 \(n^2k\),看着好像过不去。但是由于大量加速器的存在,根本跑不满,就过了。

参考代码
    namespace solution
    {
        typedef pair<int,int> PII; 
        struct TOURIST
        {
            int t,a,b;
        }tourist[N];
        int n,m,k;
        int d[N],out[N];
        vector <int> go[N];
        int arrive[N];
        void solve()
        {
            cin>>n>>m>>k;
            for(int i=1;i<n;i++) cin>>d[i];
            for(int i=1;i<=m;i++)
            {
                cin>>tourist[i].t>>tourist[i].a>>tourist[i].b;
                go[tourist[i].a].push_back(tourist[i].t);
                if(go[tourist[i].a].size() >= 2)
                {
                    int t1 = go[tourist[i].a][0],t2 = go[tourist[i].a][1];
                    go[tourist[i].a].clear();
                    go[tourist[i].a].push_back(max(t1,t2));
                }
                out[tourist[i].b] ++;
            } 
            int tim = 0;
            for(int i=1;i<=n;i++)
            {
                arrive[i] = tim;
                if(go[i].size())
                    {tim = max(tim,go[i][0]);}
                tim += d[i];
            }
            while(k--)
            {
                int maxn = 0,maxx = 0;
                for(int i=2;i<=n;i++)
                {
                    int tot = 0;
                    if(!d[i-1]) continue;
                    for(int j=i;j<=n;j++) 
                    {
                        tot += out[j];
                        if(go[j].size() && arrive[j] <= go[j][0]) break;
                    } 
                    if(tmp > maxn) {maxn = tmp;maxx = i;}
                }   
                d[maxx-1] --;
                for(int i=maxx;i<=n;i++)
                {
                    arrive[i] --;
                    if(go[i].size() && arrive[i] < go[i][0]) break;
                }
            }
            int res = 0;
            for(int i=1;i<=m;i++) res += (arrive[tourist[i].b]-tourist[i].t);
            cout<<res<<"\n";
        }
    }
posted @ 2024-10-24 20:57  SXqwq  阅读(11)  评论(0)    收藏  举报