CodeForces 3D Least Cost Bracket Sequence 题解

这道题(不看范围)一看就可以dp。

\(dp[i][j]\)表示考虑到第i位,左括号的数量减右括号的数量 的值为j。

可以推出:\(dp[i][j]=max(dp[i-1][j-1]+a[i],dp[i-1][j+1]+b[i])\)

观察发现,有些dp值是没有用的,不需要转移,可以用一个vector存下有用的dp值。

#include<bits/stdc++.h>
using namespace std;
 
typedef long long llint;
typedef pair< int, int > par;
 
const int MaxN = 50005;
const int INF = 1000000000;
const llint inf = 1LL<<60;
 
char s[ MaxN ];
 
llint dp[2][ MaxN ];
char f[2][ MaxN ];
int l[2][ MaxN ], c[2];
int a[ MaxN ], b[ MaxN ];
int fw[ MaxN ];
 
vector< par > v;
 
int main( void ) {
  scanf( "%s", s );
  int n = strlen( s );
 
  for( int i = 0; i < n; ++i ) {
    if( s[i] == '(' ) b[i] = INF;
    if( s[i] == ')' ) a[i] = INF;
    if( s[i] == '?' ) scanf( "%d %d", a+i, b+i );
  }
 
  for( int i = 0; i <= n; ++i )
    dp[0][i] = dp[1][i] = inf;
 
  v.reserve( 100*n );
 
  dp[0][0] = 0;
  l[0][ c[0]++ ] = 0;
  int now = 1, next = 0;
  for( int i = 0; i <= n; ++i ) {
    now ^= 1, next ^= 1;
 
    int tmp = v.size();
    fw[i] = tmp;
 
    c[next] = 0;
    llint d;
    for( int w = 0; w < c[now]; ++w ) {
      int j = l[now][w];
     
      if( f[now][j] ) v.push_back( par( i, j ) );
 
      d = dp[now][j];
      if( j > 0 && d+b[i] < dp[next][j-1] ) {
	if( dp[next][j-1] >= inf ) l[next][ c[next]++ ] = j-1;
	dp[next][j-1] = d+b[i], f[next][j-1] = 0;
      }
 
      if( j+1 <= ( n-i-1 ) && d+a[i] < dp[next][j+1] ) {
	if( dp[next][j+1] >= inf ) l[next][ c[next]++ ] = j+1;
	dp[next][j+1] = d+a[i], f[next][j+1] = 1;
      }
      
      if( i != n ) dp[now][j] = inf;
    }
 
    if( v.size() > tmp ) sort( v.begin() + tmp, v.end() );
  }
  fw[n+1] = v.size();
 
  if( dp[now][0] >= inf ) { puts( "-1" ); return 0; }
 
  int x = n, y = 0;
  printf( "%lld\n", dp[now][0] );
 
  for( int i = n; i > 0; --i ) {
    int lo = fw[i], hi = fw[i+1]-1;
    while( lo < hi ) {
      int mid = ( lo+hi )/2;
      if( v[mid].second > y ) hi = mid-1; else
	lo = mid;
    }
 
    int w = 0;
    if( lo <= hi && v[lo].second == y ) w = 1;
 
    if( w == 1 ) s[i-1] = '(', y--; else
      s[i-1] = ')', y++;
  }
  puts( s );
 
  return 0;
}

记忆化搜索也可以。

#include<bits/stdc++.h>

using namespace std;
#define REP(i,n) for(int i=0;i<(n);++i)
#define FORE(i,c) for(__typeof((c).begin())i=(c).begin();i!=(c).end();++i)
typedef long long ll;
 
const int N=51000;
const ll inf = ll(1000000000)*10000000;
int n,A[N],B[N],zz;
char b[N],z[N];
map<unsigned int,ll> mapka;
 
ll rek(int i, int otw) {
   unsigned int key = (unsigned int)i*N+(otw+2);
   if (mapka.find(key) != mapka.end()) return mapka[key];
   if (otw<0) return inf;
   if (otw>n-i) return inf;
   if (i==n) return otw==0 ? 0 : inf;
   if (b[i]==')') {
      return mapka[key] = rek(i+1,otw-1);
   }
   else if (b[i]=='(') {
      return mapka[key] = rek(i+1,otw+1);
   }
   else {
      return mapka[key] = min( rek(i+1,otw-1) + B[i], rek(i+1,otw+1) + A[i] );
   }
}
 
void pr(int i, int otw) {
   if (i==n) return;
   if (b[i]==')') z[zz++]=')', pr(i+1,otw-1);
   else if (b[i]=='(') z[zz++]='(', pr(i+1,otw+1);
   else if (rek(i,otw) == rek(i+1,otw-1) + B[i])
      z[zz++]=')', pr(i+1,otw-1);
   else z[zz++]='(', pr(i+1,otw+1);
}
 
int main() {
   gets(b); n=strlen(b);
   REP(i,n) if (b[i]=='?') scanf("%d%d",&A[i],&B[i]);
   ll ans=rek(0,0);
   if (ans>=inf) puts("-1");
   else {
      printf("%lld\n",ans);
      pr(0,0);
      puts(z);
   }
}

再看看数据范围,\(n \leq 5 \times 10 ^ 4\)\(O(n^2)\) 实在是危险,不过我们还有优化呢,不怕

事实上,这两个程序都是过不了的,虽然还有小的优化余地,但是加上,在\(CF\)上用处不大。

考虑正解。有各种贪心。

首先比较简单的两种,用优先队列的。

第一种,我们把左括号、右括号都看成问号,原来的左括号变成左括号的代价是\(0\),变成右括号的代价为\(inf\),原来的右括号同理。可以知道,这样它们会强制变回原来的类型(否则一定无解)。计算每个位置上两个代价的差 ,\(dif_i=a_i-b_i\)

这样以后,把所有都强制变成右括号。从左往右扫,在一定的时间,把前面的一个右括号变成左括号。这里,选\(dif_i\)最小的那个,这样才能让代价最低。

什么时候这样做呢?实在不能不改时,我们再改(明显,改得越迟,最小的\(dif_i\)也越小)。所以,在前面序列长度是奇数时,就改(如果长度为偶数改,最后一个可能变成右括号,再晚的话,更不行)。用优先队列维护所有可行括号的\(dif\)值。

(注意,代码中实现有些不同,比如0-indexed,没有真的写出\(dif\),和优先队列那里取了相反数)

#include<bits/stdc++.h>

using namespace std;
 
typedef unsigned uint;
typedef long long Int;
typedef vector<int> vint;
typedef pair<int,int> pint;
#define mp make_pair
 
template<class T> void pv(T a, T b) { for (T i = a; i != b; ++i) cout << *i << " "; cout << endl; }
int in_c() { int c; for (; (c = getchar()) <= ' '; ) { if (!~c) throw ~0; } return c; }
int in() {
    int x = 0, c;
    for (; (uint)((c = getchar()) - '0') >= 10; ) { if (c == '-') return -in(); if (!~c) throw ~0; }
    do { x = (x << 3) + (x << 1) + (c - '0'); } while ((uint)((c = getchar()) - '0') < 10);
    return x;
}
 
const Int INF = 1001001001001LL;
 
int N;
char S[50010];
 
int main() {
    int i;
    Int a, b;
    Int ans = 0;
    priority_queue< pair<Int,int> > q;
    
    scanf("%s", S);
    N = strlen(S);
    for (i = 0; i < N; ++i) {
        switch (S[i]) {
            case '(': {
                a = 0;
                b = INF;
            } break; case ')': {
                a = INF;
                b = 0;
            } break; default: {
                S[i] = ')';
                a = in();
                b = in();
            }
        }
        ans += b;
        q.push(mp(b - a, i));
        if (~i & 1) {
            ans -= q.top().first;
            S[q.top().second] = '(';
            q.pop();
        }
    }
    if (ans >= INF) {
        puts("-1");
    } else {
        printf("%lld\n", ans);
        puts(S);
    }
    
    return 0;
}

第二种,不对原来的左右括号特殊处理,用括号问题的常规操作,存下
当前前缀左括号数量减右括号数量的值$ now \(。把所有"?"当作右括号,放入一个优先队列(set),当某次\)now < 0$时,再把前面一个右括号修改。其他基本相同。

代码用了set,所以正着写。当然还用优先队列也可以。

#include<bits/stdc++.h>

std::set<std::pair<int,int> > S;
char s[55555];
int a[55555];
int b[55555];
 
int main()
{
	long long res=0;
	int i,n,now;
	scanf("%s",s);
    n=strlen(s);
    for(i=0;i<n;i++)if(s[i]=='?')scanf("%d%d",&a[i],&b[i]);
    now=0;
    for(i=0;i<n;i++)
	{
		if(s[i]=='(')now++;
		else if(s[i]==')')now--;
		else
		{
			s[i]=')';
			res+=b[i];
			now--;
            S.insert(std::make_pair(a[i]-b[i],i));
		}
		if(now<0)
		{
			if(S.empty())
			{
				puts("-1");
				return 0;
			}
			now+=2;
            res+=S.begin()->first;
            s[S.begin()->second]='(';
            S.erase(S.begin());
		}
	}
	if(now!=0)
	{
		puts("-1");
		return 0;
	}
	printf("%lld\n%s",res,s);
}

还有比较奇葩的做法。

其一。不妨把所有问号,全改成代价较小的那种,然后再慢慢调整。

维护两个set \(up\)\(down\)\(cur\)还是维护左右括号差值。

每遇到一个问号,更改它的类型后,按照新的类别插入set,'('插入\(down\),')'插入\(up\),同时设一个参数\(dif = |a_i-b_i|\),作为pair第一个元素,下标作为第二个(对于\(pair\),set先按照第一个元素排,再按照第二个元素排)。\(up\)直接用来改,\(down\) 留到后面用。

也是在\(cur<0\)时,从 \(up\) 里拿出 \(dif\) 最小的(\(up\) 里全是右括号),更改那个位置。还要把 \(down\) 清空,因为这里前缀和已经小了,以后再把前面的左改成右,一定又不合法了。

终于到了用 \(down\) 的时候了。也是每次取 \(dif\) 最小的,但改之后,必须要检查不考虑左右括号总数时,序列是否合法。这个直接拿线段树维护,这里合法的条件是,所有位置的最小值不小于\(0\)。重复执行,直到左右括号总数相等。

运行过程中,发现 \(up\)\(down\) 不够用,直接\(-1\)就好了。

#include<bits/stdc++.h>

#define mp make_pair
#define sz(v)((int)((v).size()))
#define all(v) v.begin(),v.end()
#define pb push_back
 
using namespace std;
 
typedef pair<int, int> ii;
typedef long long int64;
typedef vector<int> vi;
 
template<class T> T abs(T x) {return x > 0 ? x : (-x);}
template<class T> T sqr(T x) {return x * x;}
 
int a[100000][2];
 
void save()
{
    printf("-1\n");
    exit(0);
}
 
const int n = 65536;
 
int tree[57000 * 4 + 10];
int delta[57000 * 4 + 10];
 
void Set(int pos, int newval, int i = 1, int l = 0, int r = n - 1)
{
    if (l == r)
    {
        tree[i] = newval;
        return;
    }
    int m = (l + r) / 2;
    if (pos <= m)
        Set(pos, newval, i * 2, l, m);
    else
        Set(pos, newval, i * 2 + 1, m + 1, r);
    tree[i] = min(tree[2 * i], tree[2 * i + 1]);
}
 
void multiinc(int l0, int r0, int val, int i = 1, int l = 0, int r = n - 1)
{
    if (l >= l0 && r <= r0)
    {
        delta[i] += val;
        return;
    }
    if (min(r, r0) < max(l, l0)) return;
    int m = (l + r) / 2;
    multiinc(l0, r0, val, 2 * i, l, m);
    multiinc(l0, r0, val, 2 * i + 1, m + 1, r);
    tree[i] = min(tree[2 * i] + delta[2 * i], tree[2 * i + 1] + delta[2 * i + 1]);
}
 
int get(int l0, int r0, int i = 1, int l = 0, int r = n - 1)
{
    if (min(r, r0) < max(l, l0))
        return 100000000;
    if (l >= l0 && r <= r0)
        return tree[i] + delta[i];
    int m = (l + r) / 2;
    return delta[i] + min(get(l0, r0, 2 * i, l, m), get(l0, r0, 2 * i + 1, m + 1, r));
}
 
int main()
{
    memset(tree, 0, sizeof(tree));
    string s;
    cin >> s;
    for (int i = 0; i < sz(s); i++)
    {
        if (s[i] != '?') continue;
        scanf("%d%d", &a[i][0], &a[i][1]);
    }
    set<ii> up, down;
    int cur = 0;
    int64 cost = 0;
    for (int i = 0; i < sz(s); i++)
    {
        if (s[i] == '?')
        {
            if (a[i][0] < a[i][1])
            {
                s[i] = '(';
                cost += a[i][0];
                down.insert(ii(a[i][1] - a[i][0], i));
                cur++;
            }
            else
            {
                s[i] = ')';
                cost += a[i][1];
                up.insert(ii(a[i][0] - a[i][1], i));
                cur--;
            }
        }
        else
        {
            cur += s[i] == '(' ? 1 : -1;
        }
        if (cur < 0)
        {
            if (up.empty()) save();
            set<ii>::iterator it = up.begin();
            cost += it->first;
            s[it->second] = '(';
            cur += 2;
            up.erase(it);
            down.clear();
        }
    }
    int ss = 0;
    for (int i = 0; i < sz(s); i++)
    {
        ss += s[i] == '(' ? 1 : -1;
        Set(i + 1, ss);
    }
    while (cur > 0)
    {
        if (down.empty()) save();
        set<ii>::iterator it = down.begin();
        int delt = it->first;
        int ind = it->second;
        down.erase(it);
        multiinc(ind + 1, sz(s), -2);
        int opt = get(1, sz(s));
        if (opt < 0)
        {
            multiinc(ind + 1, sz(s), 2);
            continue;
        }
        cur -= 2;
        s[ind] = ')';
        cost += delt;
    }
    cout << cost << "\n" << s << "\n";
    return 0;
}

其二。先想办法把无解情况排除。由左右括号的数量和问号的数量,可以算出有多少问号要改成左或右括号,分别记为\(A\)\(B\)

最可能让序列合法的改法是,前\(A\)个放左括号,后\(B\)个放右括号。如果这都不行,肯定无解。

然后就以这个改好的序列为基础,找最优解。类似前面,对于所有问号,设\(dif=a_i-b_i\)(没有绝对值),按照这个(从小到大)把它们的id排序。

排完序,排在前面的\(b_i\)肯定比\(a_i\)大的较多,我们想把它们改成左括号。

神奇的来了。现在设一个指针 \(lst\) ,表示最后一个由问号转过来,且没有被当前过程确认过的左括号的下标,维护先不谈。

如果新序列中该位置是'(',先保留。否则,我们尽量尝试把它改成左括号,同时把 \(lst\) 位置上的改成右括号。这样改肯定是希望最大的了,如果更靠前,变化量只会更大。用前缀和维护之。

于是,会发现 \(lst\) 是单调不升的,用类似two-pointers的写法就行。原因是,\(lst\)右边全是右括号或者已经固定的左括号,新的操作并不能改变右边的局面(右边出现更改,顶多是把右括号变成固定的左括号,\(lst\)一定在当前操作位置的左边)。

#include<bits/stdc++.h>

using namespace std;
 
#define mp make_pair
#define pb push_back
#define rep(i,n) for(int i = 0; i < n; i++)
#define re return
#define fi first
#define se second
#define sz(x) ((int) (x).size())
#define all(x) (x).begin(), (x).end()
#define sqr(x) ((x) * (x))
#define y0 y3487465
#define y1 y8687969
 
typedef vector<int> vi;
typedef long long ll;
typedef long double ld;
typedef pair<int, int> ii;
typedef vector<ii> vii;
typedef vector<string> vs;
typedef vector<vi> vvi;
 
template<class T> T abs(T x) {
	re x > 0 ? x : -x;
}
 
int n;
int m, last;
char w[100000], ww[100000];
int a[100000], b[100000], dd[100000], prev[100000];
vector<pair<int, int> > v;
 
int check (int x) {
	if (last == -1) return 0;
	for (int i = x; i >= last; i--) 
		if (dd[i] < 2)
			return 0;
	for (int i = last; i <= x; i++) dd[i] -= 2;
	ww[last] = ')';
	ww[x + 1] = '(';
	last--;
	while (last >= 0 && (w[last] != '?' || ww[last] == ')')) last--;
	return 1;
}
                         
int go (int a, int b) {
	int d = 0;
	for (int i = 0; i < n; i++) {
		char c = w[i];
		if (c == '?')
			if (a > 0) { last = i; c = '('; a--; } else { c = ')'; b--; }
		d += int (c == '(') - int (c == ')');
		if (d < 0) return 1;
		ww[i] = c;
		dd[i] = d;
	}
	if (d != 0) return 1;
	return 0;
}
                 
int main() {
	gets (w);
	n = strlen (w);
	int d = 0, m = 0;
	for (int i = 0; i < n; i++)
		if (w[i] == '?') {
			m++;
			scanf ("%d%d", &a[i], &b[i]);
			v.push_back (make_pair (a[i] - b[i], i));
		} else d += int (w[i] == '(') - int (w[i] == ')');
	int A = (m - d) / 2, B = (m + d) / 2;
	if ((m + d) % 2 || A < 0 || B < 0) {
		printf ("-1\n");
		return 0;
	}
       	sort (v.begin (), v.end ());
       	last = -1;
       	if (go (A, B)) {
       		printf ("-1\n");
       		return 0;
       	}
	long long ans = 0;
	for (int i = 0; i < v.size (); i++) {
		int j = v[i].second;
//		printf ("%s %s %d %I64d\n", ww, w, last, ans);
		if (ww[j] == '(') {
			ans += a[j]; 
			w[j] = '(';
			if (last == j) {
				last--;
				while (last >= 0 && (w[last] != '?' || ww[last] == ')')) last--;
			}
		} else
		if (ww[j] == ')') {
			if (check (j - 1)) {
				ans += a[j]; 
				w[j] = '(';
			} else {
				ans += b[j];
				w[j] = ')';
			}
		} 
	}
       	if (go (0, 0)) {
       		printf ("-1\n");
       		return 0;
       	}
	printf ("%lld\n%s\n", ans, w);
	return 0;
}

原写于1月30日,为保护上课隐私,设此密码。

posted @ 2020-03-03 19:12  beacon_cwk  阅读(16)  评论(0)    收藏  举报