【比赛小结和题解】Codeforces Global Round 1 注意贪心,简单dp技巧,还有AC自动机+数位dp

从重要的题开始
题解连接

1110H - Modest Substrings

题意:
求长度为n的字符串,最多有多少个子串x,满足L<=x<=R, |L|<=|R|<= 800 , n <= 2000 , 都是10进制表示

这是一道数位dp好题!感谢fdf大佬的指教。fdf的题解
我们把所有子串的贡献放在它第一次脱离限制的地方统计。而脱离限制可以枚举l或者r的前缀再枚举下一维统计。这里我们需要固定该串的长度,因为脱离限制后任意填,只要长度合法即可
如果[l] < |r| - 1,还需要统计长度介于两者之间的串。
把所有脱离限制的串插入AC自动机(记录每个状态所有脱离限制的合法长度),则只要匹配到该节点,并且长度合法即可贡献答案。并且所有fail树的祖先作为该状态的后缀也可以贡献答案。
f(i,u)表示长度为i,匹配到AC自动机上u,的最优答案。转移的时候预处理g(x,i)表示节点x,还剩长度i可以贡献的答案数。
因为要求字典序最小,所以记忆化搜索,逐位确定最优解
代码分类讨论写得很长,但是注释还是比较清晰的

总结:
L == R的特判打错了,调了1个小时。这样的错误真的必须仔仔细细的把每个代码的细节看清楚,不能借助对拍!
为什么还是对拍出错就知道哪里错了,而自己看代码却查不出来。必须改正自己调题的恶习,必须静心把每个地方的错误看出来!代码真的要一句一句读!
刻骨铭心的教训啊!

#include
using namespace std;

#define rep(i,l,r) for(register int i = l ; i <= r ; i++)
#define repd(i,r,l) for(register int i = r ; i >= l ; i--)
#define rvc(i,S) for(register int i = 0 ; i < (int)S.size() ; i++)
#define rvcd(i,S) for(register int i = ((int)S.size()) - 1 ; i >= 0 ; i--)
#define fore(i,x)for (register int i = head[x] ; i ; i = e[i].next)
#define forup(i,l,r) for (register int i = l ; i <= r ; i += lowbit(i))
#define fordown(i,id) for (register int i = id ; i ; i -= lowbit(i))
#define pb push_back
#define prev prev_
#define stack stack_
#define mp make_pair
#define fi first
#define se second
#define lowbit(x) (x&(-x))

typedef long long ll;
typedef long double ld;
typedef unsigned long long ull;
typedef pair<int,char> pr;

const ll inf = 2e18;
const int N = 2e4 + 10;
const int maxn = 2020;
const ll mod = 1e9 + 7;

char s1[maxn],s2[maxn];
int a[maxn],b[maxn];
int lenl,lenr,n;
int f[maxn][N],nxt[N][10],fail[N],g[N][maxn],trans[N][10],tot;

void init(){
	int cur = 0;
	rep(i,1,lenl) a[i] = s1[i] - '0';
	rep(i,1,lenr) b[i] = s2[i] - '0';
	if ( lenl < lenr ){ //上、下界长度不同分开考虑
		cur = 0;
		rep(i,1,lenl){
			rep(j,a[i] + 1,9){ //枚举脱离限制的地方。自由节点固定长度。每个子串在刚好脱离限制的时候统计答案、因为只要长度够一定贡献
				if ( !nxt[cur][j] ) nxt[cur][j] = ++tot;
				int x = nxt[cur][j];
				g[x][lenl - i]++;
			}
			if ( !nxt[cur][a[i]] ) nxt[cur][a[i]] = ++tot;
			cur = nxt[cur][a[i]];
		}	
		g[cur][0]++;//刚好顶满下界也合法
		
		cur = 0;
		rep(i,1,lenr){
			rep(j,0,b[i] - 1){
				if ( i == 1 && !j ) continue; //小于上界不能有前导0
				if ( !nxt[cur][j] ) nxt[cur][j] = ++tot;
				int x = nxt[cur][j];
				g[x][lenr - i]++;
	
			}
			if ( !nxt[cur][b[i]] ) nxt[cur][b[i]] = ++tot;
			cur = nxt[cur][b[i]];
		}	
		g[cur][0]++;

		if ( lenr > lenl + 1 ){ //长度介于上下界只要无前导0即可
			cur = 0;
			rep(i,1,9){
				int &x = nxt[cur][i];
				if ( !x ) x = ++tot;
				rep(j,lenl + 1,lenr - 1) g[x][j - 1]++; //j - 1为还剩的长度
			}
		}
	}
	else{ //上下界长度相同,看什么时候分离
		int id = lenl + 1;
		rep(i,1,lenl){
			if ( a[i] != b[i] ){
				rep(j,a[i] + 1,b[i] - 1){ //刚好分离的时候上下界同时考虑
					if ( !nxt[cur][j] ) nxt[cur][j] = ++tot;
					int x = nxt[cur][j];
					g[x][lenl - i]++;
				}
				id = i + 1;
				break;
			}
			if ( !nxt[cur][b[i]] ) nxt[cur][b[i]] = ++tot;
			cur = nxt[cur][b[i]];
		}
		if ( id > lenl ){ //在最后一位分离要特判
			if ( a[lenl] == b[lenl] ){ //上下界完全相同,只有一种情况贡献答案
			//	if ( !nxt[cur][b[lenl]] ) nxt[cur][b[lenl]] = ++tot;
			//	cur = nxt[cur][b[lenl]];
			//这个时候cur已经匹配到最后一位了!
				g[cur][0]++;
			}
			else{ //上下界最后一位不同,恰好顶满分别统计,如果没有顶前面已经统计
				if ( !nxt[cur][b[lenl]] ) nxt[cur][b[lenl]] = ++tot;
				int x = nxt[cur][b[lenl]];
				g[x][0]++;
				if ( !nxt[cur][a[lenl]] ) nxt[cur][a[lenl]] = ++tot;
				x = nxt[cur][a[lenl]];
				g[x][0]++;
			}
		}
		else{
			int y = cur;
			if ( !nxt[cur][a[id - 1]] ) nxt[cur][a[id - 1]] = ++tot;
			cur = nxt[cur][a[id - 1]]; 
			rep(i,id,lenl){
				rep(j,a[i] + 1,9){
					if ( !nxt[cur][j] ) nxt[cur][j] = ++tot;
					int x = nxt[cur][j];
					g[x][lenl - i]++;
				}
				if ( !nxt[cur][a[i]] ) nxt[cur][a[i]] = ++tot;
				cur = nxt[cur][a[i]];
			}
			g[cur][0]++;
			cur = y;
			if ( !nxt[cur][b[id - 1]] ) nxt[cur][b[id - 1]] = ++tot;
			cur = nxt[cur][b[id - 1]];
			rep(i,id,lenr){
				rep(j,0,b[i] - 1){
					if ( !nxt[cur][j] ) nxt[cur][j] = ++tot;
					int x = nxt[cur][j];
					g[x][lenr - i]++;
				}
				if ( !nxt[cur][b[i]] ) nxt[cur][b[i]] = ++tot;
				cur	 = nxt[cur][b[i]];
			}
			g[cur][0]++;
		}
	}
}
int q[N],hh,tt;
void print_trie(){
	int x = 0;
	tt = hh = 0;
	q[tt++] = x;
	while ( hh < tt ){
		int x = q[hh++];
		cout<<x<<" : "<<endl;
		rep(i,0,9){
		
			if ( nxt[x][i] ){
				q[tt++] = nxt[x][i];
				cout<<i<<" "<<nxt[x][i]<<endl;
			}			
		}
		cout<<endl;
	}
}
void build(){
	rep(i,0,9) if ( nxt[0][i] ) q[tt++] = nxt[0][i];
	while ( hh < tt ){
		int x = q[hh++];
		rep(i,0,9){
			if ( nxt[x][i] ){
				int y = nxt[x][i] , p = fail[x];
				while ( p && !nxt[p][i] ) p = fail[p];
				fail[y] = nxt[p][i];
				q[tt++] = y;
			}
		}
	}
	rep(i,0,tt - 1){ //所有fail树上的祖先恰好不重不漏的统计了匹配到当前状态,还剩长度i,以当前节点为后缀的子串贡献次数(只统计在这里脱离限制的子串
		int x = q[i] , y = fail[x]; //进队顺序祖先一定先被计算
		rep(j,1,n) g[x][j] += g[x][j - 1];
		rep(j,0,n){
			g[x][j] += g[y][j];
		}
	}
//	print_trie();
	rep(x,0,tot){ //更新trans不能漏掉0号节点
		rep(j,0,9){
			int p = x;
			while ( p && !nxt[p][j] ) p = fail[p];
			trans[x][j] = nxt[p][j];
		}
	}
}
pr from[maxn][N];
bool vis[maxn][N];
int DP(int n,int m){ //用记忆化搜索,直接求出最小字典序。保证从前往后转移最优
	if ( !n ) return 0;
	if ( vis[n][m] ) return f[n][m];
	vis[n][m] = 1; int &d = f[n][m];
	rep(i,0,9){
		int to = trans[m][i];
		int cur = DP(n - 1,to) + g[to][n - 1];
		if ( cur > d ){
		   	d = cur;
			from[n][m] = mp(to,i);
		}
	}
	return d;
}
void solve(){
	printf("%d\n",DP(n,0));
	int cur = 0;
	rep(i,1,n){
		a[i] = from[n - i + 1][cur].se;
		cur = from[n - i + 1][cur].fi;
	}
/*	memset(f,-1,sizeof(f));
	f[0][0] = 0;
	rep(i,0,n - 1){
		rep(j,0,tot){
			if ( f[i][j] == -1 ) continue;
			rep(k,0,9){
				int to = trans[j][k] , len = n - i - 1;
				int d = f[i][j] + g[to][len];
				if ( f[i + 1][to] < d ) f[i + 1][to] = d , pre[i + 1][to] = mp(j,k);
			}
		}
	}
	int ans = 0 , rec = 0;
	rep(i,0,tot){
		if ( f[n][i] > ans ){
			ans = f[n][i] , rec = i;
		}
	}
	printf("%d\n",ans);
	repd(i,n,1){
		a[i] = pre[i][rec].se;
		rec = pre[i][rec].fi;
	}*/
	rep(i,1,n) printf("%d",a[i]);
	puts("");
}
int main(){
	freopen("input.txt","r",stdin);
	scanf("%s %s %d",s1 + 1,s2 + 1,&n);
	lenl = strlen(s1 + 1) , lenr = strlen(s2 + 1);
	init();
	build();
	solve();
}


1110G - Tree-Tac-Toe

题意:
给出一棵树,上面有白点和未染色点,白色先手,轮流染色。当染成3个连续白点获胜。问是平局还是白胜。 n <= 5e5
这道贪心很好!
首先通过加点把白点转化成无色。加点只要保证进行相同的染色后先后手不变,并且状态和以前一致。
这样转化大大减少了分类讨论的情况。即使不转化也能讨论。但是转化后模型简单很多!

接着进行特殊情况分类讨论。
考场上想得太马虎,漏掉了骨头型加中间点为奇数的情况(当时只讨论了中间一个点,没想到扩展还可以)
在确认自己算法的时候要更加仔细,当然这样的思维要多加锻炼才行!

my code

1110F - Nearest Leaf

题意:给出按dfs序标号的树,询问一个点离区间[l,r]中最近叶子的距离

这道题直接做因为不知道lca在哪里,很难维护、再加上dfs序给定,无法进行点分和重链剖分。
此时考虑离线(本来是听显然的思路,考场上问了wxh才想到,很不应该)
直接维护每个点到所有叶子的距离,统一处理询问即可

1110E - Magic Stones

题意:可以对序列c[i]进行操作: 选 取 位 置 i , ( 2 < = i < = n − 1 ) , 将 c [ i ] 变 成 c [ i + 1 ] + c [ i − 1 ] − c [ i ] 选取位置i,(2<=i<=n - 1),将c[i] 变成 c[i + 1] + c[i - 1] - c[i] i(2<=i<=n1),c[i]c[i+1]+c[i1]c[i]
问是否能将c[i]变成b[i]

每次操作想当于把差分数组交换位置,问题变成比较差分序列排序后是否相同和第一个元素是否相同
显然,如果上述相同,原序列是相同的。并且最后一个元素 c[n] = sigma(d[i]) + c[1] , 不随差分数组的顺序改变,不用特判。
判断序列相同的思路经常有转化为差分或者前缀和判断!

1110D - Jongmah

题意:给出n个数字,可以用来组成两种三元组(i,i + 1,i + 2) , (i,i,i)问最多多少个三元组

一开始看到1e6以为是贪心。其实应该简单的dp(问了队友)
因为3个以上重复的顺子直接用3个三元组代替,所以顺子个数%3记录即可。f(i,0…2,0…2) 表示以i开头,i在中间的顺子个数。简单转移即可。注意舍掉非法状态

看似非常简单的模型,有时用贪心,有时用dp处理。积累的太少,要学会总结和融会贯通、找性质!

你可能感兴趣的:(codeforces,比赛小结,题解,综合思维题)