2021.杭电中超 第三场

  1. Forgiving Matching FFT/NTT、 字符串匹配问题

大意:给定一个长度为 n 的字符串 S, 以及长度为 m 的模板串 T。字符串有 ‘0’-'9’以及通配符‘*’组成,通配符可以变成任意的字符,现规定两个字符串不一样的位置不超过 k 个就算匹配,求 k = 0 , 1 , 2 , . . . . . . ∣ T ∣ k=0,1,2,......|T| k=0,1,2,......T 字符串 S 中和 T 匹配的字符串个数。

思路:通过这道题算是长见识了。FFT/NTT 是可以用来求解字符串匹配问题的,尤其是在处理含有通配符或者不完全匹配的问题上,FFT/NTT 和KMP 相比更有优势。

首先我们先不考虑通配符的问题。 F(x) 表示 S 中以 下标 x 结尾 的 子字符串和 T 匹配的字符个数,当 S i = T j S_i=T_j Si=Tj F ( i − j + m − 1 ) F(i-j+m-1) F(ij+m1)++(模拟一下容易看出)。显然直接暴力枚举是不行的。我们仔细想想要求的是什么,假设我们确定以某个子字符串结尾下标为 x, F(x)实际通过的i,j, 满足 i-j+m-1=x,来求的 x。这不就是求卷积嘛。把 i,j 搞成次幂,最终系数就是我们要求的F(x)。当然对于 -j 的处理我们可以通过加个偏移量,除此之外,我们可以考虑把字符串 T 翻转,这样 j 就变成了 m-1-j。i-j+m-1 就变成了 i+j。接下来就是具体怎么求出 F 的点值表示法进而求出系数表示法。因为字符种类不多,我们可以去枚举字符,当等于当前枚举的字符时设置为系数设置为1,否则设置为0,只有全都为1,即字符相等时才有贡献。然后就是考虑通配符的问题。总的匹配的字符的个数=不考虑通配符匹配的个数+S中通配符的个数+T中通配符的个数-S和T通配符匹配的个数。就是个简单容斥。

代码如下:

#include 
using namespace std;
const int N=200010,M=998244353;
typedef long long LL;
int n,m,limit,l,R[N<<2],sum[N],ans[N];
LL A[N<<2],B[N<<2],f[N<<2];
char s[N<<2],t[N<<2];
LL qmi(LL a,LL b)
{
    LL res=1;
    while(b)
    {
        if(b&1)res=res*a%M;
        a=a*a%M;
        b>>=1;
    }
    return res%M;
}
void init()
{
    memset(s,0,sizeof(s));//注意s,t一定要初始化,因为我们遍历的时候用的 limit
    memset(t,0,sizeof(t));
    memset(ans,0,sizeof(ans));
    memset(sum,0,sizeof(sum));
    memset(f,0,sizeof(f));
    limit=1,l=-1;
    while(limit<=n+m) limit<<=1,l++;
    for(int i=0;i>1]>>1)|((i&1)<>_;
    while(_--)
    {
        cin>>n>>m;
        init();
        cin>>s>>t;
        reverse(t,t+m);
        for(int k=0;k<=10;k++)
        {
            char ch;
            if(k<=9)ch='0'+k;
            else ch='*';
            for(int i=0;i=m)f[i]=(f[i]-sum[i-m]+M)%M;
        }
        for(int i=m-1;i

总结:FFT/NTT 求字符串匹配问题关键在于通过卷积求 F数组,进而处理相关问题。

  1. Game on Plane 思维、签到,

思路:用优先队列从后往前贪心就好了。

  1. ​ Kart Race 前缀和、签到

思路:简单的前缀和问题,如果某段区间内存在类型1,相当于从类型1的位置往后计算颜色,所以维护每个位置最左边的1在哪就行了。最后和255取个min

代码如下:

#include 
using namespace std;
const int N=100010;
int sum[N][4],lt[N];
int n,q;
int main()
{
    int _;
    scanf("%d",&_);
    while(_--)
    {
        scanf("%d%d",&n,&q);
        for(int i=1,op,x;i<=n;i++)
        {
            scanf("%d%X",&op,&x);
            lt[i]=(op==1) ? i : lt[i-1];
            for(int j=3;j>=1;j--)
            {
                sum[i][j]=x%256;
                x/=256;
                sum[i][j]+=sum[i-1][j];
            }
        }
        for(int i=1,l,r;i<=q;i++)
        {
            scanf("%d%d",&l,&r);
            if(l<=lt[r])l=lt[r];
            for(int j=1;j<=3;j++)printf("%02X",min(255,sum[r][j]-sum[l-1][j]));
            puts("");
        }
    }
    return 0;
}
  1. Rise in Price IDA* 、动态规划

大意:给定一个 n*n 的地图,从起点为(1,1),终点为(n,n)。每个位置都有两个数字 a i , j a_{i,j} ai,j b i , j b_{i,j} bi,j 每次只能向下或者向右走, S a S_a Sa表示走到终点处a的数字之和, S b S_b Sb表示走到终点b的数字之和。求 S a ∗ S b S_a * S_b SaSb的最大值。n<=100, a i , j , b i , j a_{i,j},b_{i,j} ai,j,bi,j均为正数。

思路:这题大致扫一眼,这不是一道很简单的dp吗?然后迅速的敲完代码,然后果断wa。其实仔细想想这题是没有最优子结构的。很容易举出反例,假设 2*2d的地图,在(1,2)时 a为 1000,b为 1,但是在 (2,1)时候a=500,b=3。那么我们会从 (2,1)的时候转移过来,但是如果(2,2)处a=1,b=50。那么显然从(1,2)处转移过来是最优的。所以我们是不能用简单的dp写的。

方法1:时间复杂度并不是很有,但是思路很新奇,觉得有必要写一下。因为n<=100,数据还是很小的,所以就放心大胆的搞。我们在动态规划转移的时候虽然不能确定哪个是最有的,但是我们可以很容易知道哪个一定不是最优的,我们可以把一定不是最优的给去掉,然后把所有可能的结果都存下来。那么什么时候一定不是最优的呢?显然对于两对 ( a 1 , b 1 ) , ( a 2 , b 2 ) (a_1,b_1),(a_2,b_2) (a1,b1),(a2,b2) a 1 < a 2 , b 1 < b 2 a_1a1<a2,b1<b2 那么对于 ( a 1 , b 1 ) (a_1,b_1) (a1,b1)显然是可以去掉的,因为再怎么样它都不会比 ( a 2 , b 2 ) (a_2,b_2) (a2,b2)更优,在实现的时候我们可以用单调队列或单调栈实现。总的时间复杂度 O ( n 3 ) O(n^3) O(n3)

代码如下:

#include 
using namespace std;
const int N=110;
typedef long long LL;
typedef pair PII;
PII stk[N*N*N];
int top;
vector dp[N][N];
int a[N][N],b[N][N],n;
void check(PII x)
{
    while(top&&stk[top].second<=x.second)top--;
    if (!top || stk[top].first < x.first) stk[++top] = x;
}
void cale(vector a,vector b,vector &ans)
{
    top=0;
    int sa=a.size(),sb=b.size();
    int i=0,j=0;
    while(i> _;
    while(_ --)
    {
        cin>>n;
        for(int i=1;i<=n;i++)
            for(int j=1;j<=n;j++)
                cin>>a[i][j],dp[i][j].clear();

        for(int i=1;i<=n;i++)
            for(int j=1;j<=n;j++)
                cin>>b[i][j];

        dp[1][1].push_back({a[1][1],b[1][1]});
        for(int i=1;i<=n;i++)
            for(int j=1;j<=n;j++)
            {
                if(i==1&&j==1)continue;
                cale(dp[i][j-1],dp[i-1][j],dp[i][j]);
                for(auto &x:dp[i][j])x.first+=a[i][j],x.second+=b[i][j];
            }
        LL ans=0;
        for(auto x:dp[n][n])ans=max(ans,(LL)x.first*x.second);
        cout<

方法2:既然没有最优子结构,那我们就爆搜呗,直接搜肯定不行,得剪枝,想来想去也就也没有什么好的剪枝策略,没办法了,只能设计预估函数了,还以为永远遇不到 IDA*了,这下子终于给遇到了。接下来就是想想怎么设计预估函数。虽然我们没办法求权值之和的乘积,但是要求a,b的权值之和的最大值还是很好求的。我们先预先求出来,即已知S=A+B。求A * B 的最大值,我们当前位置的权值之和为

a,b。则最理想的情况下最终结果为 (A+a)*(B+b)=(A+a) *(S-A+b) 化简得: − A 2 + ( S + b − a ) A + x y -A^2+(S+b-a)A+xy A2+(S+ba)A+xy 二次函数求最值。当2A=S+b-a取最值。

代码如下:

#include 
using namespace std;
typedef long long LL;
const int N=110;
int n,a[N][N],b[N][N],dp[N][N];
LL ans;
LL cale(int aa,int bb,int S)
{
    int A=(S+bb-aa)/2;
    int B=S-A;
    return (LL)(A+aa)*(B+bb);
}
void dfs(int x,int y,int aa,int bb)
{
    if(x==n&&y==n)
        return (void)(ans=max(ans,1LL*aa*bb));
    
    if(xans)dfs(x+1,y,aa+a[x+1][y],bb+b[x+1][y]);
    if(yans)dfs(x,y+1,aa+a[x][y+1],bb+b[x][y+1]);
}
int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    int _;
    cin>>_;
    while(_--)
    {
        cin>>n;
        ans=0;
        for(int i=1;i<=n;i++)for(int j=1;j<=n;j++)cin>>a[i][j];
        for(int i=1;i<=n;i++)for(int j=1;j<=n;j++)cin>>b[i][j];
        for(int i=0;i<=n+1;i++)for(int j=0;j<=n+1;j++)dp[i][j]=0;
        for(int i=n;i>=1;i--)
            for(int j=n;j>=1;j--)
            {
                int sum=a[i][j]+b[i][j];
                if(i==n&&j==n)dp[i][j]=sum;
                else dp[i][j]=max(dp[i+1][j],dp[i][j+1])+sum;
            }
        dfs(1,1,a[1][1],b[1][1]);
        cout<

总结:逆向思维的动态规划很奇妙,IDA*真的牛,预估函数设计的好快到飞起。

  1. Segment Tree with Pruning 签到,记忆化搜索

大意:本来是构建线段树时当l=r即区间长度为 1 时return ,现在改成(r - l + 1 <= k) return ; 问总共需要多少个节点。

思路:比赛时想的是模拟构建往下推,写的很麻烦,实际上直接写个记忆化搜索就好了,但是l,r<=1e18,数组存不下,所以用unorder_map存。

代码如下:

#include 
using namespace std;
typedef long long LL;
LL n,k;
unordered_map dp;
LL dfs(LL l,LL r)
{
    if(r-l+1<=k)return 1;
    if(dp.count(r-l+1))return dp[r-l+1];
    LL v=1;
    LL mid=l+r >> 1;
    v+=dfs(l,mid);
    v+=dfs(mid+1,r);
    dp[r-l+1]=v;
    return v;
}
int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    int _;
    cin>>_;
    while(_--)
    {
        cin>>n>>k;
        cout<

总结:多次用到同一内容可以考虑同dp,dp+搜索=记忆化搜索。

你可能感兴趣的:(杭电,c语言,算法)