ABC175-F-字符串+巧妙建图跑最短路

前言:

这道题非常简洁且有趣。将字符串和建图完美结合在一起。

题目大意:

给你 n n n个字符串,每个字符串有一个花费 C i C_i Ci。让你将他们拼接起来形成一个回文串使得花费最小.

n ≤ 50 , ∣ s ∣ ≤ 20 , C i ≤ 1 e 9 n \leq 50 , |s| \leq 20,C_i\leq1e9 n50,s20,Ci1e9

题目思路:

这题过程我就理解了仨小时。。。

1.从最终答案出发:假设答案存在并且是由 k k k个字符串构成(可能重复)。 a i a_i ai序列代表下标序列.

s a 1 + s a 2 + . . . + s a k s_{a_1}+s_{a_2}+...+s_{a_k} sa1+sa2+...+sak

2.考虑将其化简成更小的问题形式(这一点很重要).

因为他是回文串,所以 s a 1 s_{a_1} sa1 s a k s_{a_k} sak一定会有一部分是对称的。并且一定是前后缀关系:例如: a b c d abcd abcd b a ba ba.

那么将这个对称的部分给消除掉,一定会导致 s a 1 s_{a_1} sa1 s a k s_{a_k} sak里其中一个字符串成为空串。这里我们假设 s a k s_{a_k} sak变为空串.那么问题形式变成:

s a 1 ′ + s a 2 + . . . + s a k − 1 s_{a_1}'+s_{a_2}+...+s_{a_{k-1}} sa1+sa2+...+sak1,    s a 1 ′ \ \ s_{a_1}'   sa1代表消去一部分前缀的 s a 1 s_{a_1} sa1.它是 s a 1 s_{a_1} sa1的后缀.

这个时候我们的整体字符串变短了,这么递归下去一定能够消除完整个回文串。

3.我们可以得到递归时每层消除字符串的做法:

只关注回文串最两端的字符串,将短的那个整个字符串给消灭掉。

4.发现这个过程这样的性质后,我们能够做什么?

4.1:不难发现,任何一个完整的回文串的构造过程都能够像上面那样逆序的消解(到最后变成一个单独的回文串或者空集).

4.2:在递归的过程中,我们总是找到当前回文串一端字符串的前缀/后缀 (这取决于你取哪一端),让它去与另一端的整个字符串进行消解。

那么我们可以利用这个递归过程来建图:

获取每一个字符串的后缀,代表它被放在左边。获取每一个字符串的前缀,代表它被放在右边。

那么这张图的节点集就是:整体字符串(它其实也属于前后缀)+他们的每一个前缀+他们的每一个后缀.(注意需要将他们的空集考虑进去)

然后对每一个[前后缀],我们都与所有 [完整后前缀] 尝试进行连边(当他们构成回文边界关系时).边权为[完整前后缀]所属的字符串的花费。

然后由于每一个[完整后缀]都有可能被放在左边第一个。所以我们开一个超级源点连接所有完整后缀(也就是整体字符串).边权为字符串的花费

再考虑到递归出口是:当我们剩下来的前后缀 s ′ s' s是回文串时,就可以停止构造了。那么就开一个汇点。让所有本身就是回文的前后缀连向它,边权为0.

不难发现,这张图从源点跑最短路到汇点即为最终答案。

AC代码:

#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define pii pair<int,ll>
#define pb push_back
#define mp make_pair
#define vi vector<int>
#define vll vector<ll>
#define fi first
#define se second
const int maxn = 50 + 5;
const int mod = 1e9 + 7;
string a[maxn];
ll cost[maxn];
int n , tot;
map<pii,int> id[2];
map<int,pii> way[2];
vector<pii> e[1000005];
// 左边第i个字符串的第x个后缀 和 右边第j个字符串的第y个前缀 是否能匹配
// 返回{-1,-1} 代表不合法.  t 表征剩下的字符是左边还是右边
pii check (pii l , pii r , int & t)
{
    
    
    string x = a[l.fi].substr(l.se);
    string y = a[r.fi].substr(0 , r.se + 1);
    if (x.size() >= y.size()){
    
    
        t = 0;
        // abc ba
        string pre = x.substr(0 , y.size());
        reverse(pre.begin(),pre.end());
        if (pre != y) return {
    
    -1 , -1};
        return {
    
    l.fi , l.se + y.size()};
    }
    t = 1;
    // ab  cba
    string aft = y.substr(y.size() - x.size() , x.size());
    reverse(aft.begin(),aft.end());
    if (aft != x) return {
    
    -1 , -1};
    return {
    
    r.fi , r.se - x.size()};
}
bool check_p (int t , pii d)
{
    
    
    // 特判空字符串
    if (t == 0 && d.second == (int)a[d.first].size()) return true;
    if (t == 1 && d.second == -1) return true;
    string x;
    if (t == 0) x = a[d.fi].substr(d.se);
    else x = a[d.fi].substr(0 , d.se + 1);
    string y = x;
    reverse(y.begin(),y.end());
    return x == y;
}
ll dp[1000005];
int bk[1000005];
struct Node
{
    
    
    int id;
    ll val;
    bool operator < (const Node & a) const
    {
    
    
        return val > a.val;
    }
};
int main()
{
    
    
    ios::sync_with_stdio(false);
    cin >> n;
    for (int i = 1 ; i <= n ; i++){
    
    
        cin >> a[i] >> cost[i];
        int m = a[i].size();
        for (int t = 0 ; t <= 1 ; t++)
            for (int j = 0 ; j < m ; j++)
                id[t][mp(i , j)] = ++tot , way[t][tot] = mp(i , j);
        // 两个空字符串
        id[0][mp(i , m)] = ++tot;
        way[0][tot] = mp(i , m);
        id[1][mp(i , -1)] = ++tot;
        way[1][tot] = mp(i , -1);
    }
    // 建边
    ++tot; // 源点 = 0 , 终点 = tot
    for (int i = 1 ; i <= n ; i++){
    
    
        int m = a[i].size();
        for (int l = 0 ; l <= 1 ; l++){
    
    
            for (int j = 0 ; j < m ; j++){
    
    
                // 判断是不是回文,是的话就没必要连转移边,直接连接至终点
                if (check_p(l , {
    
    i , j})) continue;
                // 否则.我们这个前缀后缀一定是连一个整串
                for (int k = 1 ; k <= n ; k++){
    
    
                    if (i == k) continue;
                    int t = 0 , len = a[k].size();
                    pii res;
                    if (l) res = check({
    
    k , 0} , {
    
    i , j} , t);
                    else res = check({
    
    i , j} , {
    
    k , len - 1} , t);
                    if (res.fi == -1) continue;
                    e[id[l][{
    
    i , j}]].pb({
    
    id[t][res] , cost[k]});
                    // 输出相关信息
                   /* if (l) cout << "右边 ";
                    else cout << "左边 ";
                    cout << i << " " << j << ' ';
                    cout << "成功与第" << k << "个字符串连上一条边" << endl;
                    cout << "他们将转移到" << res.first << " " << res.second << endl;*/
                }
            }
        }
    }
    // 源点连 整左字符串
    for (int i = 1 ; i <= n ; i++) e[0].pb({
    
    id[0][{
    
    i , 0}] , cost[i]});
    // 回文串连 终点
    for (int i = 0 ; i <= 1 ; i++){
    
    
        for (auto g : way[i]){
    
    
            if (check_p(i , g.se)) e[g.fi].pb({
    
    tot , 0});
        }
    }
    priority_queue<Node> q;
    memset(dp , -1 , sizeof dp);
    dp[0] = 0;
    q.push({
    
    0 , 0});
    while (q.size()){
    
    
        int u = q.top().id; q.pop();
        if (bk[u]) continue;
        bk[u] = 1;
        for (auto g : e[u]){
    
    
            int v = g.fi;
            ll w = g.se;
            if (dp[v] == -1 || dp[v] > dp[u] + w){
    
    
                dp[v] = dp[u] + w;
                q.push({
    
    v , dp[v]});
            }
        }
    }
    cout << dp[tot] << endl;
    return 0;
}

猜你喜欢

转载自blog.csdn.net/qq_35577488/article/details/114600456