2021.牛客暑假多校 第十场 补题

  1. Browser Games 字符串哈希/字典树压缩

大意:给定 n 字符串,每个字符串长度不超过100。对于每个字符串可以通过他的某个前缀去查询到它的信息。并确保任意一个字符串都不是另一个字符串的前缀。定义 ans[i]为 查询到前 i 个字符串的信息,并且确保查不到后面字符串的信息的最少前缀字符串的个数。最终依次输出ans[i]。(此题空间限制非常严格,只能 O ( n ) O(n) O(n)

思路1:字符串哈希。对于直接查询 n 个字符串我们很好统计数量,直接让每个前缀都是对应字符串的第一个字符,然后去重就是最少数量了,我们考虑从后往前做。对于去掉第 n 个字符串的时候,我们可以比遍历该字符串的前缀,通过字符串哈希,如果和我们之间处理的查询前缀有冲突的话,那么显然是不合题意的,因为会查到字符串 n 的信息,我们只需要把那些有冲突的查询前缀往后多加1个字符就好了。

具体代码如下:

#include <bits/stdc++.h>
using namespace std;
const int N=100001,bas=131; // N 写成 1000010就会爆内存
typedef unsigned long long ULL;
unordered_map<ULL,vector<int> >mp;
ULL p[N];
string s[N];
int n,pos[N],ans[N];
int main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);
	cin>>n;
	for(int i=1;i<=n;i++)
	{
		cin>>s[i];
		p[i]=s[i][0]; // 存字符串哈希值
		pos[i]=0; //纪录每个字符串的查询前缀已经加到了哪个位置
		mp[p[i]].push_back(i);
	}
	for(int i=n;i>=1;i--)
	{
		ans[i]=mp.size();
		ULL pre=0;
		for(auto &c:s[i]) //遍历当前字符串的前缀,看是否和查询前缀存在冲突
		{
			pre=pre*bas+c;
			auto it=mp.find(pre);
			if(it==mp.end())continue; //存在冲突
			for(auto &id:it->second)
			{
				if(id==i)continue;
				++pos[id];
				p[id]=p[id]*bas+s[id][pos[id]];
				mp[p[id]].push_back(id);
			}
			mp.erase(it);
		}
	}
	for(int i=1;i<=n;i++)cout<<ans[i]<<"\n";
	return 0;
}

方法2:字典树缩点

一看到和前缀相关的问题,我们下意识想到的就是字典树写。如果不考虑空间的限制。我们可以先将所有的字符串建立成字典树。因为不存在任意一个字符串是另一个的前缀,显然我们直接用每个字符串做查询前缀一定是可以的。但是我们要最小化前缀的数量,并且不能是未加入集合的字符串的前缀,所以在字典树中我们代表查询前缀尾地址的节点尽可能靠上,越往的前缀越可能是多个字符串的前缀嘛,所以对于每次我们可以让查询前缀。我们枚举 i 边加入集合边计算。对于遍历到字符串 i, 我们我们从字符串的结尾,就是在字典树中对应的叶子节点尽可能往上走,对于一个分叉点,如果该点走过的次数=该节点的儿子数则表明该前缀不会查询到未加入集合的点,可以继续往上走,否则就不能。接下来的问题就是建立字典树的问题,因为我们要从下往上走,所以我们要让每个节点指向它的父节点,我们可以递归建树。接下来就是节省内存缩点的问题,对于往后只有一个分支的节点,我们可以用一个点来代替,对于有相同前缀的一系列的点我们可以压缩成一个点,这些都是可以用递归实现的,比较考验代码能力,具体看代码注释:

#include <bits/stdc++.h>
using namespace std;
const int N=1e5+7;
struct node{
    int del,son,fa;
}tr[N*100];
int idx;
int n;
char str[N][103];
int strid[N];
int stred[N];
int getid(char c)
{
    if(c>='a'&&c<='z')return c-'a';
    if(c>='A'&&c<='Z')return c-'A'+26;
    if(c>='0'&&c<='9')return c-'0'+52;
    if(c=='.')return 62;
    return 63;
}
int dfs(int dep,int l,int r) //递归建树
{
    if(l==r)//只有一个儿子的时候就不用递归了,减少内存
    {
        int u=++idx;
        stred[strid[l]]=u;
        return u;
    }
    bool f=1;
    for(int i=l;i+1<=r;i++)
    {
        if(str[strid[i]][dep]!=str[strid[i+1]][dep])
        {
            f=0;
            break;
        }
    }
    if(f)//有相同前缀的压缩成一个点
    {
        int u=dfs(dep+1,l,r);
        if(dep)return u; //如果存在相同前缀,压缩成一个点,我们写的递归函数最终返回的根节点实际上是不存在的,只是为了构建成一棵树
        tr[u].fa=0;  //但是这里这块压缩点需要返回压缩后的点,如果开头就是一样的字符我们直接返回 u 的话则把 u 当成了虚拟节点,但实际上这个点
        return 0;  // 是可以取的,所以如果dep=0的话,我们要搞一个虚拟节点
    }
    vector<int> vt[64];
    for(int i=l;i<=r;i++) //在字典树中相同的前缀会共用节点,所以我们这里把字符相同的放到桶里分组递归,
        vt[getid(str[strid[i]][dep])].push_back(strid[i]);

    int cnt=l;
    int u=++idx;
    for(auto v:vt)
    {
        if(v.size()==0)continue;
        for(auto j:v)strid[cnt++]=j;// 重新编号
        tr[dfs(dep+1,l,l+v.size()-1)].fa=u;
        tr[u].son++;
        l+=v.size();
        
    }
    return u;
}
int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    cin>>n;
    for(int i=0;i<n;i++)
    {
        cin>>str[i];
        strid[i]=i;//存下字符串的编号,以下操作可以直接用编号完成
    }
    int rt=dfs(0,0,n-1);
    int ans=0;
    for(int i=0;i<n;i++)
    {
        ans++;
        int nw=stred[i];
        while(tr[nw].fa!=rt)
        {
            nw=tr[nw].fa;
            tr[nw].del++;
            if(tr[nw].del==tr[nw].son)ans-=tr[nw].son-1;
            else break;
        }
        cout<<ans<<"\n";
    }
    return 0;
}

总结:掌握字典树的实质,进而运用不同的建树方式,掌握字典树缩点的方法

  1. Train Wreck 思维,栈,贪心

大意:给定一串含有 ‘(‘和 ‘)’ 的字符串,确保成对存在且都为 n 个,有编号1-n 的火车,’(’ 代表火车入栈,‘)’ 代表火车出站。先给定代表火车编号的颜色序列 a, a i a_i ai 代表编号为 i 的火车颜色。现在需要调整颜色序列顺序,使得一列火车进栈时刻火车代表的颜色序列不相同。

例如 ()(())() 总共有4个进栈时刻,代表的火车编号分别为 [1],[2],[2,3],[4]。

思路:首先需要想到的是栈的操作其实和 dfs去代替。出栈操作对应着 dfs 中的回溯。因为我们直接考虑每个进栈时刻对应的火车编号,而栈又是一次性的,所有我们考虑用dfs建图,用0当做根节点。建完图之后就将问题转化成了从根节点(不算根节点)到任意节点构成的链代表的颜色序列都不相同。对于任意一个节点 u, 他有若干个儿子节点,要想确保从根节点到它的儿子节点构成的颜色序列不同,则只有当它的儿子节点的颜色不同才行。所以我们进一步将问题转化,我们分配颜色的时候只需要让每个节点的所有儿子节点颜色不同就行了,对于分配我们可以用 堆 贪心的进行分配。

代码如下:

#include <bits/stdc++.h>
using namespace std;
const int N=1000010;
typedef pair<int,int> PII;
char s[N<<1];
vector<int> g[N];
int n,cnt[N],ans[N],stk[N],top,id;
priority_queue<PII> q;
PII p[N];
bool f=1;
void dfs(int u)
{
    if(f)
    {   
        int t=0;
        for(auto v:g[u])
        {
            if(q.size()>0)
            {
                auto tmp=q.top();
                q.pop();
                ans[v]=tmp.second;
                if(tmp.first>1)p[t++]={tmp.first-1,tmp.second};
            }
            else {f=0;break;}
        }
        for(int i=0;i<t;i++) q.push(p[i]);
        for(auto v:g[u])dfs(v);
    }
}
int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    cin>>n>>(s+1);
    for(int i=1;i<=n;i++)
    {
        int x;
        cin>>x;
        cnt[x]++;
    }
    for(int i=1;i<=n;i++)if(cnt[i])q.push({cnt[i],i});
    for(int i=1;i<=(n<<1);i++)
    {
        if(s[i]=='(')
        {
            g[stk[top]].push_back(++id);
            stk[++top]=id;
        }
        else top--;
    }
    dfs(0);
    if(f)
    {
        cout<<"YES"<<"\n";
        for(int i=1;i<=n;i++)cout<<ans[i]<<" ";
    }
    else cout<<"NO"<<"\n";
    return 0;
}

总结:有关栈的问题想想能否转化成图论问题。

  1. War of Inazuma (Easy Version) 又是阅读理解,题目都读不懂

给定有 2 n 2^n 2n个点 0 − 2 n − 1 0-2^n-1 02n1 。规定两个点相邻是当且仅当两个点的二进制位不同。先将其分成两个阵营用0和1表示,要求任意一个点和他相邻的点中同阵营的不超多 n \sqrt{n} n 上去整。输出任意一个合法的。

思路:假设 A,B 相邻, A 的二进制表示有 x 个 1,B 的二进制表示 y 个 1,x!=y,因为一旦x=y, 如果 1 的位置不是一一对应就不止一个二进制位不同,一旦一一对应有必定没法找出一个二进制位不一样。所以x!=y。不妨令 x<y。因为只有一位二进制位不同,所以 y=x+1,二者奇偶性不同。所以我们就可以根据二进制位1的个数的奇偶性分阵营。

代码如下:

#include <bits/stdc++.h>
using namespace std;
int n;
int cale(int x)
{
    int cnt=0;
    while(x)
    {
        x-=(x&-x);
        cnt++;
    }
    return cnt;
}
int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    cin>>n;
    for(int i=0;i<(1<<n);i++)
    {
        if(cale(i)%2)cout<<0;
        else cout<<1;
    }
    return 0;
}

总结:过的多的人的题,实在想不出就大胆猜一下。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值