哈希

· · 算法·理论

哈希

感觉哈希不难,但是哈希运用到题目中就不一样了。这就需要一些巧妙的转化。

例题1 星战

P8819 [CSP-S 2022] 星战

高贵的总司令题。不可以!总司令!!!

哈希好题。知道了要用哈希感觉真的差不多绿及以下,主要是这个太难想了。

除了哈希,还有个就是内向基环森林的性质,in = out = n,但是 out = n 只是 in = n 的必要条件,所以加上哈希,表示从点 u 出发的哈希值,在修改后累加的哈希总和要等于原先所有 u 点的哈希之和才是一个内向基环森林。

#include <bits/stdc++.h>
using namespace std;
#define int long long
const int MAX = 5e5+5;
int n,m,w[MAX],in[MAX],now[MAX],sum,ans;
// w[i]:存从 i 点出发的边的哈希值
// in:存原图入边的哈希值
// now:存修改后当前的哈希值
mt19937 rnd;
signed main()
{
    scanf("%lld%lld",&n,&m);
    for(int i=1;i<=n;i++)
        w[i]=rnd(),sum+=w[i];
    while(m--)
    {
        int u,v;
        scanf("%lld%lld",&u,&v);
        now[v]+=w[u],in[v]+=w[u];
        ans+=w[u];
    }
    scanf("%lld",&m);
    while(m--)
    {
        int op,u,v;
        scanf("%lld",&op);
        if(op==1) // 这些操作都挺好推的,理解就行
            scanf("%lld%lld",&u,&v),now[v]-=w[u],ans-=w[u];
        else if(op==2)
            scanf("%lld",&v),ans-=now[v],now[v]=0;
        else if(op==3)
            scanf("%lld%lld",&u,&v),now[v]+=w[u],ans+=w[u];
        else if(op==4)
            scanf("%lld",&v),ans+=in[v]-now[v],now[v]=in[v];
        if(ans==sum)
            puts("YES");
        else puts("NO");
    }
    system("pause");
    return 0;
}

例题2 消消乐

说道哈希,就很容易想到这个题了

P9753 [CSP-S 2023] 消消乐

这个题容易想到用栈维护,然后可以发现,在两个时间点栈的状态相同时,那么这两个时间点中间的部分肯定都能被删除的。然后考虑将栈内元素进行哈希,开一个 unordered_map 来存某个哈希值所对的出现次数,最后统计答案。

#include <bits/stdc++.h>
using namespace std;
#define int long long
#define ull unsigned long long
const int INF = 0x7fffffff;
const int mod = 998244353;
const int MAX = 2e6+5;
const int base = 1331;
int n,ans;
ull has[MAX]; // hash[i] 存到了第 i 个元素,此时栈内元素的哈希值
char s[MAX];
unordered_map<ull,int>mp; // 记录每一种 hash 值出现了多少次
stack<pair<int,int>>st; // pair < val, pos > 栈
set<ull>ss; // 记录出现过的,可能成为答案的 hash 值
signed main()
{
    // freopen(".in", "r", stdin);
    // freopen(".out", "w", stdout);
    scanf("%lld",&n);
    cin>>s+1;
    ss.insert(0); // 记录初始栈的情况
    mp[0]++;
    for(int i=1;i<=n;i++)
        if(!st.empty()&&st.top().first==s[i])
        {
            mp[has[st.top().second-1]]++; // 累加当前贡献
            has[i]=has[st.top().second-1]; // 继承前面的哈希
            st.pop();
        }
        else
        {
            st.push({s[i],i});
            has[i]=has[i-1]*base+s[i]+1; // 处理哈希
            ss.insert(has[i]),mp[has[i]]++; // 记录可能出现的答案并且累加当前贡献
        }
    for(int i:ss)
        ans+=mp[i]*(mp[i]-1)/2; // 对于出现次数统计答案,式子很好推,就是这么多相同状态中选两个作为答案
    printf("%lld\n",ans);
    system("pause");
    return 0;
}

例题3 字符串匹配

P7114 [NOIP2020] 字符串匹配

算是一个自己如场切的哈希蓝。

这个题的话是有个超纲的 O(n) 的 Z 函数做法的,之前也写过,但是现在看不懂了(。所以写的是个 O(n\ln n) 的哈希做法,感觉比较好理解的。

#include<bits/stdc++.h>
using namespace std;
#define ull unsigned long long
const int MAX = (1<<20) + 5;
const int mod = 998244353;
const int INF = 0x7fffffff;
const int base = 19491001;
inline int read(){
    int x=0,y=1;char c=getchar();
    for(;c<'0'||c>'9';c=getchar())if(c=='-')y=-1;
    for(;c>='0'&&c<='9';c=getchar())x=(x<<1)+(x<<3)+(c^48);
    return x*y;
}
ull has[MAX],b[MAX];
int T,n,fl[26],pre[MAX],suf[MAX],sum[MAX][27];
char s[MAX];
signed main()
{
//  freopen("string.in","r",stdin),freopen("string.out","w",stdout);
    T=read();
    while(T--)
    {
        cin>>s+1;
        int n=strlen(s+1);
        for(int i=0;i<26;i++) fl[i]=0;
        b[0]=1;
        for(int i=1;i<=n;i++) has[i]=has[i-1]*base+1ull*s[i],b[i]=b[i-1]*base;
        for(int i=1;i<=n;i++)
        {
            if(fl[s[i]-'a']==0) pre[i]=pre[i-1]+1;
            else pre[i]=pre[i-1]-1;
            fl[s[i]-'a']^=1;
        }
        for(int i=0;i<26;i++) fl[i]=0;
        suf[n+1]=0;
        for(int i=n;i>=1;i--)
        {
            if(fl[s[i]-'a']==0) suf[i]=suf[i+1]+1;
            else suf[i]=suf[i+1]-1;
            fl[s[i]-'a']^=1;
        }
        for(int i=1;i<=n;i++) for(int j=0;j<=26;j++) sum[i][j]=sum[i-1][j]+(pre[i]<=j);

        long long ans=0;
        for(int len=2;len<=n;len++)
        {
            ull x=has[len];
            for(int i=len;i<n;i+=len) if(has[i]-has[i-len]==x*b[i-len])
                ans+=sum[len-1][suf[i+1]];else break;
        }
        printf("%lld\n",ans);
    }
    return 0;
}

例题 4

CF1977D

很好的一个哈希题。考虑对于每一列只填一个 1,枚举这个 1 填在哪里,发现确定了这一列之后全局都确定了,接着就是哈希一下,统计所有修改哈希值出现次数最多的即可。

#include<bits/stdc++.h>
using namespace std;
#define int long long
#define ull unsigned long long
#define pii pair<int,int>
#define ls (root << 1)
#define rs (root << 1 | 1)
#define mid ((l + r) >> 1)
#define fi first
#define se second
#define SZ(a) ((int)(a).size())
#define END(a) (prev((a).end()))
#define pc(a) putchar(a)
#define pb(a) push_back(a)
#define yes return puts("YES"),void()
#define no return puts("NO"),void()
typedef long long ll;
const int N = 3e5 + 5;
const int mod = 998244353;
const ll INFLL = LONG_LONG_MAX;
const int INF = INT_MAX;
inline int read()
{
    int x=0,y=1;char c=getchar();
    for(;c<'0'||c>'9';c=getchar())if(c=='-')y=-1;
    for(;c>='0'&&c<='9';c=getchar())x=(x<<1)+(x<<3)+(c^48);
    return x*y;
}
unordered_map<ull,int>f;
unordered_map<ull,pii>g;
mt19937 rnd;
int n,m,mx;
ull hs[N],mxhs;
vector<int>a[N];
string ans;
inline void solve()
{
    n=read(),m=read(),ans.clear(),f.clear(),g.clear();
    for(int i=1;i<=n;i++)hs[i]=1ull*rnd()*rnd();mx=mxhs=0;
    for(int i=1;i<=n;i++){a[i].resize(m+5);for(int j=1;j<=m;j++)scanf("%1lld",&a[i][j]);}
    for(int j=1;j<=m;j++){
        ull now=(1-a[1][j])*hs[1];
        for(int i=2;i<=n;i++)now+=a[i][j]*hs[i];f[now]++,g[now]={1,j};
        for(int i=2;i<=n;i++)now+=hs[i-1]*((a[i-1][j]==1)?1:-1),now+=hs[i]*((a[i][j]==0)?1:-1),f[now]++,g[now]={i,j};
    }
    for(auto i:f)if(i.se>mx)mx=i.se,mxhs=i.fi;
    printf("%lld\n",mx);
    int x=g[mxhs].fi,y=g[mxhs].se;
    for(int i=1;i<=n;i++)ans.pb((i==x&&a[i][y]||i!=x&&!a[i][y])?'0':'1');
    cout<<ans<<'\n';
    return ;
}
signed main()
{
//  freopen(".in","r",stdin);
//  freopen(".out","w",stdout);
    int T=read();srand(time(0));while(T--) solve();system("pause");return 0;
}