21 ICPC 区域赛 沈阳 EFBJM 题解

gym链接

很有意思的比赛,尤其是顺势吃了去年的瓜

暂时补了 \(EFBJM\)

E

签到

solution

判断 \(|t| = 5\) 的串在模式串出现次数,暴力枚举即可。

F

签到

solution

因为 \(n \le 10^3\),所以 \(\sum_{i=1}^n |s[i,n]| \le 10^6\)

我们暴力对所有前缀编码,然后取最值即可。

B

仍是暴力

solution

因为 \(\oplus\),所以各个位是独立的,那么我们对每个位去进行 dfs 即可。

对于一个位,不同联通块直接不产生影响,一个联通快构成一个图。
那么对其中任意一个元素设置初始值,块内其他元素的值就都可以确定了。

又因为初值可以设置 \(0/1\),那么

  • 如果都不冲突,则取较小值
  • 都冲突,则无解,输出 -1
  • 一个可行则直接加入答案。

在 gym 上这题给了 \(10s\) 不知道赛场是否相同。
我的代码应该还算常数比较大比较丑陋了。

code

int n,m,op1,op2;
vector<PII> v[N];
int a[N][2][D];
bool vis[N][2][D];

int dfs(int p,int b,int k,int &op)
{
	int sum = a[p][b][k];
	for(auto &[y,w] : v[p])
	{
		int t = (w>>k)&1;
		if(vis[y][b][k])
		{
			if(a[y][b][k] != (a[p][b][k]^t))	op = 0;
			continue;
		}
		vis[y][b][k] = true;
		a[y][b][k] = a[p][b][k]^t;
		sum += dfs(y,b,k,op);
	}
	return sum;
}

void func(void)
{
	cin >> n >> m;
	for(int i=1;i<=m;++i)
	{
		int x,y,w;
		cin >> x >> y >> w;
		v[x].push_back({y,w});
		v[y].push_back({x,w});
	}
	int ans = 0;
	for(int i=0;i<30;++i)
	{
		for(int j=1;j<=n;++j)
		{
			if(!vis[j][0][i])
			{
				int res = M;
				op1 = op2 = 1;
				a[j][1][i] = 1, a[j][0][i] = 0;

				vis[j][0][i] = true;
				int tmp = dfs(j,0,i,op2);
				if(op2)	res = min(res,tmp);
                
				vis[j][1][i] = true;
				tmp = dfs(j,1,i,op1);
				if(op1)	res = min(res,tmp);

				if(op1 || op2)	ans += (1<<i)*res;
				else
				{
					cout << "-1\n";
					return;
				}
			}
		}
	}
	cout << ans << '\n';
}

J

开始准备模拟,然后想能不能暴力,就写了个bfs

sloution

对于 \(1234 \rightarrow 2345\)\(0000 \rightarrow 1111\) 是一样的,或者说本质不同状态只有 \(10^5\) 种。

那么我们从 \(0000\) 开始跑 bfs 即可。

出于方便处理,用用 vector 处理状态了,而没有用数字。

code

map<vector<int>,int> ans;
void func(void);

signed main(void)
{
	Start;
	int _ = 1;
	queue<pair<vector<int>,int>> q;
	vector<int> t(4);
	q.push({t,0});
	while(q.size())
	{
		auto [x,y] = q.front();	q.pop();
		if(ans.count(x))	continue;
		ans[x] = y;
		for(int i=0;i<4;++i)
		{
			for(int j=i;j<4;++j)
			{
				vector<int> t1 = x,t2 = x;
				for(int k=i;k<=j;++k)
				{
					t1[k] = (t1[k]+1)%10;
					t2[k] = (t2[k]+9)%10;
				}
				if(!ans.count(t1))	q.push({t1,y+1});
				if(!ans.count(t2))	q.push({t2,y+1});
			}
		}
	}
	cin >> _;
	while(_--)	func();
	return 0;
}

void func(void)
{
	string s1,s2;
	cin >> s1 >> s2;
	vector<int> a(4);
	for(int i=0;i<4;++i)	a[i] = (s1[i]-s2[i]+10)%10;
	cout << ans[a] << '\n';
}

M

看到字符串起手就敲了个sa,然后发现没那么简单。
sam学的不好也没想出来。

最后看这篇题解,大受震撼。
ICPC2021 沈阳站 M String Problem 题解 | 十种做法一网打尽 , 一道题带你回顾字符串科技
真的太强了,要不是这个博客,我可能就只是按官解用sam或border补了

后缀数组

solution1

明显可以用 \(sa\),但是直接用 \(sa\) 明显不太可能。

答案是各个前缀的后缀,因为如果是前缀的子串,那么无脑延长为后缀肯定比用空串要大。

那么答案是各个前缀的后缀,但是后缀数组存储整个串的后缀。
对于串 \(babc\),在 \(i = 4\) 时,\(bc > babc\),也就是 \(rk_3 > rk_1\)
但是在 \(i = 3\) 时,\(bab > b\),这时不能直接使用 \(rk\) 数组。
所以不能再访问到 \(i\) 时直接用 \(i\) 来更新答案。

我们继续看 \(babc\),在 \(i \in [1,3]\) 时,答案都是 \(s[1,i]\),而在 \(i = 4\) 时,我们用 \(rk_3\) 更新了答案,因为对于 \(babc\)\(bc\),在第二位才出现不同,或者应该说是 \(lcp+1\) 出现了,不同。

对于新位置 \(i\) 和旧答案下标 \(p\)\(rk_i > rk_p\) 时,在 \(i+lcp(i,p)\) 打上标记,然后在访问到 \(i+lcp(i,p)\) 时,将答案更新为 \(i\)

这时就又有个问题了,如果在 \(i \sim i+lcp\) 的过程中 \(p\) 被更新了怎么办?
那么进行讨论,设新位置 \(p'\)\(j\) 为当前访问下标,\(i\) 为在 \(j\) 打上的标记。

  • 如果 \(rk_i < rk_{p'}\),那么继续使用 \(p\) 即可,无需更新。
  • 否则只需要进行上面对于 \(i,p\) 相同的处理,只是这里处理的对象变为标记和新答案。
    • 如果 \(lcp(p',i)\) 没有超过 \(j\),也就是 \(i + lcp(i,p') \le j\) 那么就是在 \(lcp\) 之后位置出现了不同,这时需要更新答案为 \(i\)
    • 如果 \(lcp(p',i)\) 超过了 \(j\),也就是还在 \(lcp\) 范围内,答案不需要更新,只是需要把新位置的标记更新。

solution2

吸取了一下上面博客的优化解法。

\(rk_1\) 我们肯定会使用,然后可能会使用第一个 \(> rk_1\) 的后缀,以此类推。

那么答案的 \(rk\) 就是递增的。

那么我们只需要比较相邻可能答案,他们的 \(lcp\) 是否会超过当前 \(i\) 即可。

code1

struct SA
{
	int L;
	vector<int> sa,rk,h,logk;
	vector<vector<int>> mn;
	SA(string& st)
	{
		L = st.size()-1;
		sa.resize(L+1), rk.resize(L+1), h.resize(L+1);
		build_sa(st), build_h(st);
	}
	void build_sa(const string &st)
	{
		int m = D, p = 0, len = max(m,L)+1;
		vector<int> cnt(len), lrk(len), lsa(len);
		for(int i=1;i<=L;++i)	++ cnt[rk[i] = (int)st[i]];
		for(int i=1;i<=m;++i)	cnt[i] += cnt[i-1];
		for(int i=L;i>=1;--i)	sa[cnt[rk[i]] --] = i;
		for(int w=1;p!=L;w<<=1,m=p)
		{
			int idx = 0;
			for(int i=L-w+1;i<=L;++i)	lsa[++ idx] = i;
			for(int i=1;i<=L;++i)
				if(sa[i] > w)	lsa[++ idx] = sa[i] - w;
			fill(cnt.begin(),cnt.end(),0);
			for(int i=1;i<=L;++i)	++ cnt[rk[i]];
			for(int i=1;i<=m;++i)	cnt[i] += cnt[i-1];
			for(int i=L;i>=1;--i)	
				sa[cnt[rk[lsa[i]]] --] = lsa[i];
			p = 0;
			for(int i=1;i<=L;++i)	lrk[i] = rk[i];
			for(int i=1;i<=L;++i)
			{
				if(lrk[sa[i]] == lrk[sa[i-1]] && 
				   lrk[sa[i]+w] == lrk[sa[i-1]+w])	rk[sa[i]] = p;
				else	rk[sa[i]] = ++ p;
			}
		}
	}
	void build_h(const string &st)
	{
		sa[0] = rk[0] = 0;
		for(int i=1,k=0;i<=L;++i)
		{
			if(k)	k --;
			while(st[i+k] == st[sa[rk[i]-1]+k])	++ k;
			h[rk[i]] = k;
		}
		logk.resize(L+1);
		logk[1] = 0;
		for(int i=2;i<=L;++i)	logk[i] = logk[i/2]+1;
		int k = logk[L]+1;
		mn.resize(k,vector<int>(L+1));
		for(int i=2;i<=L;++i)	mn[0][i] = h[i];
		for(int i=1;i<k;++i)
		{
			for(int j=2;j+(1<<i)-1<=L;++j)
			{
				mn[i][j] = min(mn[i-1][j],mn[i-1][j+(1<<(i-1))]);
			}
		}
	}
	int query_min(int l,int r)
	{
		int k = logk[r-l+1];
		return min(mn[k][l],mn[k][r-(1<<k)+1]);
	}
	int lcp(int i,int j)
	{
		if(i == j)	return L-i+1;
		int rki = rk[i], rkj = rk[j];
		if(rki > rkj)	swap(rki,rkj);
		return query_min(rki+1,rkj);
	}
};

void func(void)
{
	string st;	cin >>st;
	st = '_' + st;
	SA sa(st);
	int p = 1;
	vector<int> tp(sa.L+1);
	for(int i=1;i<=sa.L;++i)
	{
		if(sa.rk[i] > sa.rk[p])	tp[i+sa.lcp(i,p)] = i;
		if(sa.rk[tp[i]] > sa.rk[p])
		{
			if(tp[i]+sa.lcp(p,tp[i]) <= i)	p = tp[i];
			else	tp[tp[i]+sa.lcp(p,tp[i])] = tp[i];
		}
		cout << p << ' ' << i <<'\n';
	}
}

code2

void func(void)
{
	string st;	cin >>st;
	st = '_' + st;
	SA sa(st);
	int p = 0;
	vector<int> tp(1,1);
	for(int i=2;i<=sa.L;++i)
		if(sa.rk[i] > sa.rk[tp.back()])	tp.push_back(i);
	for(int i=1;i<=sa.L;++i)
	{
		while(p+1 < tp.size() && tp[p+1] + sa.lcp(tp[p],tp[p+1]) <= i)	++ p;
		cout << tp[p] << ' ' << i << '\n';
	}
}

后缀自动机

没用官解的在线,感觉离线还是简单些

solution

使用 DAWG 而非 link 树。

构造出 sam 后,我们从根节点遍历 DAWG,每次贪心最大字符路径。

那么构成的串,在截止位置上必然是字典序最大的。

但是对于 \(baba\),怎么保证 \(i = 4\) 的截止位置使用 \(baba\) 而非 \(ba\) 呢?
只需要给节点标记最早的下标,这样可以每个串使用最晚的标记,很明显在前缀相同时,串越长字典序越大。

code

int ans[N],ed[N];
string st;
bitset<N> vis;
// sam
int nxt[N][D],lk[N],len[N];
int rt = 1,lst = 1,idx = 1;

void extend(char c,int i)
{
	int p = lst, u = ++ idx, t = c-'a';
	lst = u, len[u] = len[p]+1;
	ed[u] = i;
	while(p && (!nxt[p][t]))
	{
		nxt[p][t] = u;
		p = lk[p];
	}
	if(!p)	lk[u] = rt;
	else
	{
		int np = nxt[p][t];
		if(len[p]+1 == len[np])	lk[u] = np;
		else
		{
			int tp = ++ idx;
			memcpy(nxt[tp],nxt[np],sizeof(nxt[np]));
			len[tp] = len[p]+1, lk[tp] = lk[np];
			ed[tp] = ed[np];
			lk[np] = lk[u] = tp;
			while(p && nxt[p][t] == np)
			{
				nxt[p][t] = tp;
				p = lk[p];
			}
		}
	}
}

void dfs(int p,int l)
{
	vis[p] = true;
	for(int i=D-1;i>=0;--i)
	{
		if(vis[nxt[p][i]])	continue;
		dfs(nxt[p][i],l+1);
	}
	if(!ans[ed[p]])	ans[ed[p]] = ed[p]-l+1;
}

void func(void)
{
	cin >> st;
	st = '_' + st;
	int L = st.size()-1;
	for(int i=1;i<=L;++i)	extend(st[i],i);
	dfs(1,0);
	for(int i=1;i<=L;++i)	cout << ans[i] << ' ' << i << '\n';
}
posted @ 2025-10-08 14:55  zerocloud01  阅读(41)  评论(0)    收藏  举报