2018Beijing H. Approximate Matching

重新学习了一遍AC自动机,之前也没有系统性地学习过,现在看起来并不是那么困难,就是在字典树上面加了一个类似KMP的跳跃数组。
下面先上AC自动机的板子。

#include <bits/stdc++.h>
#define rep( i , l , r ) for( int i = (l) ; i <= (r) ; ++i )
#define per( i , r , l ) for( int i = (r) ; i >= (l) ; --i )
#define erep( i , u ) for( int i = head[(u)] ; ~i ; i = e[i].nxt )
using namespace std;
const int maxn = 1111;
struct AC{
	int tr[maxn][26] , cnt;
	int e[maxn];
	int fail[maxn];
	void ins( char *s ){
		int u = 0;
		for( int i = 0 ; s[i] ; ++i ){
			int t = s[i] - 'a';
			if( !tr[u][t] ) tr[u][t] = ++cnt;
			u = tr[u][t]; 
		}
		e[u]++;
	}
	void calc_fail(){
		queue<int> q;
		memset( fail , 0 , sizeof fail );
		for( int i = 0 ; i < 26 ; ++i ) if( tr[0][i] ) q.push( tr[0][i] );
		while( !q.empty() ){
			int u = q.front(); q.pop();
			for( int i = 0 ; i < 26 ; ++i ){
				if( tr[u][i] ){
					fail[tr[u][i]] = tr[fail[u]][i]; // fail计算 
					q.push( tr[u][i] );
				}else{
					tr[u][i] = tr[fail[u]][i];
					//匹配到空字符,则索引到父节点fail指针对应的字符,以供后续指针的构建
                    //类似并差集的路径压缩,把不存在的tr[k][i]全部指向tr[fail[k]][i]
                    //这句话在后面匹配主串的时候也能帮助跳转
				}
			}
		}
	}
	int query( char *t ){
		int u = 0 , res = 0;
		for( int i = 0 ; t[i] ; ++i ){
			u = tr[u][t[i] - 'a'];
			for(int j = u ; j && ~e[j] ; j = fail[j] ) res += e[j] , e[j] = -1;
		}
		return res;
	} 
};
int main(){
	return 0;
}

题目大意

给你T组数据,每组包含两个正整数N,M以及字符串T,问你有多少个长度为M的字符串S,满足T在最多修改一位的情况下可以成为S的子串。

思路和分析

下午这道题搞了好久,思路是AC自动机+DP,如果正向去解决这个问题有点困难,那么我们可以从反面去考虑这个问题,即计算不满足这个性质的字符串个数。
记录 f [ i ] ] [ j ] f[i]][j] f[i]][j]为匹配到第 i i i个字符,当前落在自动机 j j j号节点上面的不满足条件的字符串个数。
那么答案显然就是 2 M − Σ i = 1.. a c . c n t f [ M ] [ i ] 2^M-\Sigma_{i=1..ac.cnt} f[M][i] 2MΣi=1..ac.cntf[M][i]

#include <bits/stdc++.h>
#define rep( i , l , r ) for( int i = (l) ; i <= (r) ; ++i )
#define per( i , r , l ) for( int i = (r) ; i >= (l) ; --i )
#define erep( i , u ) for( int i = head[(u)] ; ~i ; i = e[i].nxt )
using namespace std;
const int maxn = 2222;
struct AC{
	int tr[maxn][26] , cnt;
	int e[maxn];
	int fail[maxn];
	void ins( char *s ){
		int u = 0;
		for( int i = 0 ; s[i] ; ++i ){
			int t = s[i] - '0';
			if( !tr[u][t] ) tr[u][t] = ++cnt;
			u = tr[u][t]; 
		}
		e[u]++;
	}
	void calc_fail(){
		queue<int> q;
		memset( fail , 0 , sizeof fail );
		for( int i = 0 ; i < 26 ; ++i ) if( tr[0][i] ) q.push( tr[0][i] );
		while( !q.empty() ){
			int u = q.front(); q.pop();
			for( int i = 0 ; i < 26 ; ++i ){
				if( tr[u][i] ){
					fail[tr[u][i]] = tr[fail[u]][i]; // fail计算 
					q.push( tr[u][i] );
				}else{
					tr[u][i] = tr[fail[u]][i];
					//匹配到空字符,则索引到父节点fail指针对应的字符,以供后续指针的构建
                    //类似并差集的路径压缩,把不存在的tr[k][i]全部指向tr[fail[k]][i]
                    //这句话在后面匹配主串的时候也能帮助跳转
				}
			}
		}
	}
	int query( char *t ){
		int u = 0 , res = 0;
		for( int i = 0 ; t[i] ; ++i ){
			u = tr[u][t[i] - '0'];
			for(int j = u ; j && ~e[j] ; j = fail[j] ) res += e[j] , e[j] = -1;
		}
		return res;
	} 
}ac;

typedef long long ll;
ll f[44][maxn];
char str[111];
ll qpow( ll x , int y ){
	ll res = 1ll;
	for( ; y ; y >>= 1 ){
		if( y & 1 ) res *= x;
		x = x * x;
	}
	return res;
}
int main(){
	int T = 0;
	scanf("%d" , &T);
	while( T-- ){
		int N , M;
		ac.cnt = 0;
		scanf("%d%d%s" , &N , &M , str );
		memset( ac.tr , 0 , sizeof ac.tr );
		memset( ac.e , 0 , sizeof ac.e );
		memset( f , 0 , sizeof f );
		ac.ins( str );
		for( int i = 0 ; i < N ; ++i ){
			str[i] ^= 1;
			ac.ins( str );
			str[i] ^= 1;
		}
		ac.calc_fail();
		f[0][0] = 1;
		for( int i = 0 ; i < M ; ++i ){
			for( int j = 0 ; j <= ac.cnt ; ++j ){
				if( f[i][j] == 0 || ac.e[j] ) continue;
				for( int k = 0 ; k < 2 ; ++k ){
					int u = j;
					while( ac.tr[u][k] == 0 ) u = ac.fail[u];
					if( ac.e[ac.tr[u][k]] > 0 ) continue;
					f[i + 1][ac.tr[u][k]] += f[i][j];
				}
			}
		}
		ll ans = qpow( 2ll , M );
		for( int i = 0 ; i <= ac.cnt ; ++i ){
			ans -= f[M][i];
		}
		cout << ans << endl;
	}
	
}

需要注意这个板子有一个虚根0,注意从虚根开始统计,同时要注意cnt开始为0。
query函数这题没有用到,具体用起来也是一样,不过好像是用完就删除的,如果要多次查询需要修改一下。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值