1 条题解

  • 0
    @ 2025-8-24 22:24:23

    自动搬运

    查看原文

    来自洛谷,原作者为

    avatar pocafup
    路 是人走出来的

    搬运于2025-08-24 22:24:23,当前版本为作者最后更新于2020-09-08 11:49:15,作者可能在搬运后再次修改,您可在原文处查看最新版

    自动搬运只会搬运当前题目点赞数最高的题解,您可前往洛谷题解查看更多

    以下是正文


    Update 10.13 博客上代码不知道哪里挂了/fad,本地改了博客上可能忘改了?被dalao指出,现已更正。

    P0:题外话

    讲个笑话,正解状压。

    P1:基础观察

    首先需要证明一个结论:如果某一个括号串在点 ii 出发有不止一个完全匹配,那么选择结束点最小的匹配一定最佳。

    因为我们规定了只有完全匹配完某个字符串才能继续匹配,所以答案一定是越早匹配完越好。

    P2:题解

    下文用 kk 表示 len(k)len(k), aa 表示 len(a)len(a)


    对于 a8a\le 8 的数据,暴力枚举每种可出现的排列查最大值,ans 取 max 即可

    复杂度 O(naa)O(na^a)


    对于 n10n \le 10 且随机的数据,考虑优化全排列。

    发现可以使用 dfs 进行枚举,每当枚举到就退出。

    复杂度 O(naa)O(na^a),但在数据随机下能够过(良心出题人)。

    核心枚举代码: by

    https://www.luogu.com.cn/user/322075

    void dfs(ll pos,ll id,ll cur){
    	ans = max(ans,cur);
    	if (pos >= l) return;
    	for (ll cnt = 0;cnt < xl[id] && pos < l;pos += 1)
    		if (x[id][cnt] == k[pos]) cnt += 1,cur += v[id];
    	for (ll i = 1;i <= n;i += 1) dfs(pos,i,cur);
    }
    

    对于 n10n \le 10 且每个字符只用一次的数据,发现可以对每个位置进行状压。

    首先预处理在每个位置时分别处理后面第一次出现 ( 和 ) 的位置,此操作用倒叙枚举能够快速的求出。

    for (int i=k;i>0;i--){
        nxt[0][i] = (s[i-1]=='(') ? i : nxt[0][i+1];
        nxt[1][i] = (s[i-1]==')') ? i : nxt[1][i+1];
    }
    

    dp[i][j]dp[i][j] 表示目前匹配到 ii 点,字符串存取数量为 jj。枚举每个串暴力查 kk 进行转移即可,复杂度 O(n2nak)O(n2^nak)

    核心转移代码:

    inline void solve(int kk, int type, int pos){
      int cnt = 1, val = dp[type][kk];
      while(cnt<=a[pos].length()){
        int now = (a[pos][cnt-1])==')';
        if (!nxt[now][kk]) {ans = max(ans,val);return;}//没地方跳了
        if (cnt==a[pos].length()) val+=v[pos],dp[type+(1<<(pos-1))][nxt[now][kk]+1] = max(dp[type+(1<<(pos-1))][nxt[now][kk]+1],val);//跳完了
        else kk = nxt[now][kk]+1,val+=v[pos];
        cnt++;
      }
      ans = max(ans,val);
    }
    

    另一种可行的方法为在每个位置进行 dp,在每个位置均暴力枚举字符串找匹配 (后面会具体讲 dp 思路,这里略)。

    复杂度 O(nk2)O(nk^2),人口普查。


    对于 n100,a8n\le 100,a \le 8 的数据,考虑暴力 dp。

    dp[i]dp[i] 表示在点 ii 前能拿到的最高完美匹配,注意 dp[i]dp[i] 不包括点 ii 能拿到的。

    转移可以枚举括号串,对每个括号串暴跳预处理出来的括号。

    转移代码:

    inline void jump(int kk, int pos){
      int cnt = 1, val = dp[kk];
      while(cnt<=a[pos].length()){
        int now = (a[pos][cnt-1])==')';//转化为01
        if (nxt[now][kk]==-1) {ans = max(ans,val);return;}//没地方跳了
        if (cnt==a[pos].length()) dp[nxt[now][kk]+1] = max(dp[nxt[now][kk]+1],val+v[pos]);//跳完了
        else kk = nxt[now][kk]+1,val+=v[pos];
        cnt++;
      }
      ans = max(ans,val);
    }
    

    复杂度 O(k+nak)O(k+nak)


    对于 a8a \le 8 的数据,暴力可过,但有另外的解法(近似正解)

    发现括号只有两种形态,考虑用 01 表示,从而想到状压。

    在预处理 () 的位置后我们还可以暴力预处理每一种状态。预处理的东西跟之前意义一样,对于每个字符暴跳 ( 和 ) 即可。极限状态数 2a12^{a}-1,预处理后暴跳可以做到 O(a)O(a) 处理出某一个状态在某一个点的值。

    之后 dp 转移可以暴力枚举字符串转移,对于长度为 aa 的字符串直接转移,否则暴力枚举进行转移。

    复杂度 O(k+a2ak+nka)O(k+a2^ {a}k+nka) 怎么好像还慢了

    可以考虑枚举每种长度进行状压,于是时空变为 O(k+a22ak+nk)O(k+a^22^ak+nk)


    对于 a300a \le 300 的数据,暴力复杂度错误,考虑状压。

    然而直接状压 aa 显然是个笑话,于是考虑分块思想。

    发现可以将每个括号串进行分块处理。分成 a\sqrt{a} 块,

    只需预处理长度为 a\sqrt{a} 的字符串即可。

    对于转移,在每个点上仍然暴力枚举字符串,但我们对字符串分块跳跃。对于不足 a\sqrt{a} 长度的块暴跳即可。

    然而发现这还是过不去

    发现块长使复杂度不平衡, 瓶颈在于状压的状态,因此,我们可以尝试调节块长。

    复杂度 O(k+a2ak+nka)O(k+\sqrt{a}2^ {\sqrt{a}}k+nk\sqrt{a}),实测块长 484\sim8 都可过。

    const int MAXN = 505;
    const int MAXK = 1e4+5;
    const int L = 5;
    int n,v[MAXN],nxt[2][MAXK],dp[MAXK],k,pre[MAXK][(1<<L)+5],num[MAXN],cor[MAXN][(MAXN/L)+1],ans;
    string s,a[MAXN];
    int max(int a, int b){return a>b?a:b;}
    int solve(int pos, int num, int val){
      while(true){
        int now = (num>>val) & 1;
        if (!nxt[now][pos]) return 0;
        if (val==0) return nxt[now][pos];
        pos = nxt[now][pos]+1;val--;
      }
    }
    inline int get(int pos, int f, int t){
      int re = 0;
      for (int i=f;i<=t;i++) re += ((a[pos][i]=='(') ? 0 : 1) * (1<<(t-i));
      return re;
    }
    void jump(int pos, int ptr,int cnt,int sum, int from){
      if (cnt>a[pos].length()) {dp[ptr] = max(dp[ptr],dp[from]+sum);return;}
      while(cnt<=a[pos].length()){
        int now = (a[pos][cnt-1])==')';
        if (!nxt[now][ptr]) {ans = max(ans,dp[from]+sum);return;}//没地方跳了
        ptr = nxt[now][ptr]+1,sum+=v[pos];
        cnt++;
      }
      if (ptr!=-1)dp[ptr] = max(dp[ptr],dp[from]+sum);//跳完
      ans = max(ans,dp[from]+sum);
    }
    // pos是块的编号,cor是目前块的括号序列
    void Solve(int kk, int pos){
      int re = 0,from = kk,cnt = 1;
      for (int i=1;i<=num[pos];i++){
        if (pre[kk][cor[pos][i]]) {    // 如果有块可以跳
          kk = pre[kk][cor[pos][i]]+1;
          re += L*v[pos];
          cnt+=L;
        }else break;
      }
      jump(pos,kk,cnt,re,from);
    }
    signed main(){
      cin >> n >> s;
      k = s.length();
      for (int i=1;i<=n;i++) cin >> a[i] >> v[i];
      for (int i=1;i<=n;i++) {
        num[i] = (a[i].length()/L);
        for (int l=1;l<=num[i];l++) cor[i][l] = get(i,L*(l-1),L*l-1);
      }
      for (int i=k;i>0;i--){
        nxt[0][i] = (s[i-1]=='(') ? i : nxt[0][i+1];// 0表示 (, 1表示 )
        nxt[1][i] = (s[i-1]==')') ? i : nxt[1][i+1];
      }
      for (int i=1;i<=k;i++)
        for (int j=0;j<=(1<<L)-1;j++)
          pre[i][j] = solve(i,j,L-1);  //处理状态
      for (int i=1;i<=k;i++)
        for (int j=1;j<=n;j++)
          Solve(i,j);
      printf("%d\n",ans);
    }
    /*
    3
    ((()())(()()
    (() 4
    () 2
    ()()()() 5
    */
    
    

    审题人给出了对不足大小的块不足长度时不需暴跳的方法:

    pre[l][i][j]pre[l][i][j] 为长度 ll 的块在使用 ii 号字符串,状态为 jj 的最早完美匹配结束点。这个长度在 solve 函数内可以求出,总复杂度不变,但暴跳时直接处理即可。

    对于不是完美匹配的块,我们将 pre[l][i][j]pre[l][i][j] 内的值变为在某个长度下最多能拿的匹配数量。为了区分结束点与长度,我们可以将这个数字设为负数,然后在累计时乘以 -1 即可。

    然而不知道为啥我这么写跑得更慢了,代码不贴了,有兴趣可以找我要。


    P3:可能存在的疑惑

    为什么 std 在块大小不足 a\sqrt{a} 时要暴力转移呢?

    可以发现,假设块大小为 8,用 0 表示 (,1 表示 ),则 (((((((( 的表示法为 0000000, 即 0。

    然而,对于大小为 6 的小块,(((((( 的表示法同为 0,但两者代表的东西不同。故我们不能直接使用状态进行转移。

    • 1

    信息

    ID
    5786
    时间
    1000~2200ms
    内存
    128MiB
    难度
    6
    标签
    递交数
    0
    已通过
    0
    上传者