第一周训练总结

本周学习的东西:

  • [ ] 概率dp

  • [ ] 取模运算下的逆元

  • [ ] 爆搜

  • [ ] 状态亚索dp

  • [ ]

6.26

个人赛补题

C Ideal Sheet(两个矩阵覆盖能否形成第三个矩阵)

大意:用两个矩阵A,B能否通过覆盖重叠得到第三个矩阵C,长宽均小于等于10

思路:首先,矩阵AB确定相对位置,共O(20^4),然后,再对构造好的ans矩阵进行检查,观察是否能从中匹配到矩阵C,匹配的时间复杂度为O(30^2)

之所以C的匹配长度是30,是因为题意中“无穷”的含义,在于C可以从任何一个位置开始进行匹配(当然超出C本身左右10位置的无意义),所以,从C加上左右各10的位置,长宽共30的矩阵进行匹配。

补题中发现的细节:

①开数组的时候不妨多开几个,因为这种高暴力小空间的题目,往往空间不会超,但是如果开小了就很容易SF

②bool型只占一个字节,int占4个,可以考虑用这点来减少空间

③debug的时候,数组太长很难观察,那就指定看某一个或者某维的,便于发现规律。

#include<bits/stdc++.h>
using namespace std;
int a[15][15], b[15][15], c[35][35];
int ans[40][40];
int ha, wa, hb, wb, hc, wc;

int main()
{
    cin >> ha >> wa;
    char kk[15];
    for (int i = 1; i <= ha; i++)
    {
        cin >> (kk + 1);
        for (int j = 1; j <= wa; j++)
        {

            if (kk[j] == '#')a[i][j] = 1;
            else a[i][j] = 0;
            //cout << a[i][j] << " ";
        }
        //cout << endl;
    }

    cin >> hb >> wb;


    for (int i = 1; i <= hb; i++)
    {
        cin >> (kk + 1);
        for (int j = 1; j <= wb; j++)
        {

            if (kk[j] == '#')b[i][j] = 1;
            else b[i][j] = 0;
            //cout << b[i][j] << " ";
        }
        //cout << endl;
    }
    cin >> hc >> wc;


    for (int i = 1; i <= hc; i++)
    {
        cin >> (kk + 1);
        for (int j = 1; j <= wc; j++)
        {

            if (kk[j] == '#')c[i + 10][j + 10] = 1;
            else c[i + 10][j + 10] = 0;
            //cout << c[i][j] << " ";
        }
        //cout << endl;
    }
    for (int i = 1; i <= 10 + hc; i++)
    {
        for (int j = 1; j <= 10 + wc; j++)
        {
            for (int k = 1; k <= 10 + hc; k++)
            {
                for (int l = 1; l <= 10 + wc; l++)
                {
                    memset(ans, 0, sizeof(ans));
                    for (int q = 1; q <= ha; q++)
                    {
                        for (int w = 1; w <= wa; w++)
                        {
                            if (a[q][w])ans[i + q - 1][j + w - 1] = 1;
                        }
                    }
                    for (int q = 1; q <= hb; q++)
                    {
                        for (int w = 1; w <= wb; w++)
                        {
                            if (b[q][w])ans[k + q - 1][l + w - 1] = 1;
                        }
                    }

                        int f = 1;
                  //if(i==3)
                        for(int q = 1; q <= 30; q++)
                        {
                            for (int w = 1; w <= 30; w++)
                            {
                                //cout << ans[q][w] << " ";
                                if (c[q][w] != ans[q][w])
                                {
                                    f = 0;
                                }
                            }
                            //cout << endl;
                        }
                        if (f)
                        {
                            cout << "Yes" << endl;
                            return 0;
                        }


                }
            }
        }
    }
    cout << "No" << endl;
}

E - Distinct Adjacent(一圈n人,m数编号,相邻不同)

题目大意:给一圈n个人,m个数,用m个数给这圈人编号,相邻两个编号要求不一样。

重点在于考虑圈首和圈尾的人。可以基于倒数第二的人,分类讨论为两种情况:

①倒数第二与圈首一样,那圈尾的选择有m-1种。

②倒数第二和圈首不一样,那圈尾的选择有m-2种。

为了把这个问题理解为子问题,我们可以把人数为i时候的方法数,“倒数第二”理解为第i-1个人时候的方法数

所以我们在每次新增的时候,可以理解为两种状态转移的形式

dp[0][i+1]=dp[0][i](m-2)+dp[1][i](m-1)

dp[1][i+1]=dp[0][i]

dp[?][i]表示从第一个人到第i个人的方法数

0代表第一个人和最后一个人不同,1代表相同

#include<bits/stdc++.h>
using namespace std;
long long dp[1000005][3];
int main()
{
    long long int n, m;
    cin >> n >> m;
    dp[1][1] = m;
    for (int i = 2; i <= n; i++)
    {
        dp[i][1] = dp[i - 1][0];
        dp[i][0] = dp[i - 1][1] * (m - 1);
        dp[i][0] += dp[i - 1][0] * (m - 2);
        dp[i][1] %= 998244353;
        dp[i][0] %= 998244353;
    }
    cout << dp[n][0];
}

*数论

推广到第n项:

$ans=(m-1)^{n}+(m-1)*(-1)^{n}$

G - Approximate Equalization Editorial(数列,相邻数操作,最终差值小于1)

大意:给定一个数列,有两种操作:①a[i]--,a[i+1]++②a[i]++,a[i+1]--(1≤i≤n-1)

要求最后任意a[i]与a[j]之间的差都小于1

分析:基于给定的操作,我们可以知道整个数列的和tot是不会改变的。

假设avg为tot/n向下取整,那么数列里一定有tot-avg*n个数,值为(avg+1),剩下的值均为avg。

由于每一次变的数,并不改变总和,所以我们可以把每一次改变看为一个子问题:dp[i][j]代表前i位中有j个数为avg+1的操作数。

在a[i]中,每一个数最终都要变为avg或avg+1,这是必然的。同时,每次在将前i个数改变后,若要将状态转换到i+1,那也只考虑a[i+1]的改变,而a[i+1]的操作不会影响前i个数的大小

所以,根据以上推论,可以得到状态转移方程:(dp数组初始化为大值)

$dp[i+1][j+1]=min(dp[i+1][j+1],dp[i][j]+abs(a[i+1]'-(avg+1)))$

$dp[i+1][j]=min(dp[i][j+1],dp[i][j]+abs(a[i+1]'-avg))$

#include<bits/stdc++.h>
#include<cmath>
using namespace std;
#define int long long //记得开longlong
const int N = 5010;
int a[N], pre[N], dp[N][N];
int n, avg, tot;
unsigned main()
{
    cin >> n;
    memset(pre, 0, sizeof(pre));
    tot = 0;
    for (int i = 1; i <= n; i++)
    {
        cin >> a[i];
        pre[i] = pre[i - 1] + a[i];//预处理前i位和
        tot += a[i];
    }
    avg = tot / n;
    int x = n-(tot - avg * n);//最终在n位数中会有x位数的大小为avg+1
    memset(dp, 0x3f3f3f3f, sizeof(dp));
    dp[0][0] = 0;
    for (int i = 0; i <= n - 1; i++)
    {
        for (int j = 0; j <= i; j++)
        {
            int val = j * (avg + 1) + (i - j) * avg; //dp[i][j]当前情况下理想前a[i]位的和
            int minus = val - pre[i];//dp[i][j]下得到理想第a[i+1]'的步数=理想值val-实际值pre[i]
            int A = a[i+1]-minus;//a[i+1]'的值=a[i+1]的值-操作步数mins
            dp[i+1][j] = min(dp[i+1][j], dp[i][j] + abs(A - avg));
            //在j不变的情况下,考虑dp[i][j]从A变为avg的步数
            dp[i + 1][j + 1] = min(dp[i + 1][j + 1], dp[i][j] + abs(A - avg - 1));
            //j变为j+1的情况下,考虑dp[i][j]从A变为avg+1的步数

          /*debug用
          cout << "i: " << i << " j:" << j << endl;
            cout<<"  val: " << val << "  mins:  " << mins << " A:  " << A << endl;
            for (int p = 0; p <= n; p++)
            {
                for (int q = 0; q <= n; q++)
                {
                    if (dp[p][q] < 0x3f3f3f3f)
                        cout << dp[p][q] << " ";
                    else cout << "@ ";
                }
                cout << endl;
            }*/

        }
    }
    cout << dp[n][n - x];
}

6.28

个人赛补题:

分数取模(求取模意义下的逆元)

拓展欧几里得法

对一个大数取模,需要使用拓展欧几里得定理:

void exgcd(int a, int b, int& x, int& y) {
  if (b == 0) {
    x = 1, y = 0;
    return;
  }
  exgcd(b, a % b, y, x);
  y -= a / b * x;
}

快速幂法

小费马定理:

$a^{p-1}mod (p)=1mod(p),改动后:a^{p-2}=a^{-1}mod(p)$

故对b/a mod p有

$(b\div a) mod (p)=(b\times a^{p-2}) mod(p)$

故有快速幂取模法:

int qpow(long long a, int b) {
  int ans = 1;
  a = (a % p + p) % p;
  for (; b; b >>= 1) {
    if (b & 1) ans = (a * ans) % p;
    a = (a * a) % p;
  }
  return ans;
}

快速幂法使用了 费马小定理,要求b为一个素数。而拓展欧几里得只要求gcd(a,b)=1;

线性求逆元

inv[1] = 1;
for (int i = 2; i <= n; ++i) {
  inv[i] = (long long)(p - p / i) * inv[p % i] % p;
}

F - Minimum Bounding Box 2

题意:给定一个n*m个小方块组成的矩形,从中随机选出k个小方块并计算积分,求积分的期望

其中,积分指能够覆盖k个小方块的最小矩形的所含小方块个数.

参考:AtCoder Beginner Contest 297 D - F - 知乎以及axr的讲解

我们可以把问题拆分成多个子问题:(积分矩阵,指i*j大小的矩阵中容纳k个所选方块)

①有多少种选择积分矩阵的方式:(n-i+1)*(m-j+1)

②一个积分矩阵内,有多少种合法放置k个方块的方式:(容斥原理)

$C0-C1-C2-C3-C4+C1C2+C2C3+C3C4+C1C3+C1C4+C2C4-C1C2C3-C1C2C4-C2C3C4+C1C2C3C4$

③期望=总积分/总选择数

#include <bits/stdc++.h>

#define x first
#define y second
#define endl '\n'
#define int long long
#define NO {puts("NO"); return;}
#define YES {puts("YES"); return;}

using namespace std;

typedef long long ll;
typedef pair<int, int> PII;

const int MOD = 998244353;
const int N = 1e6 + 10, INF = 1e18;

int n, m, k;
int base;
int fact[N], infact[N];

int qmi(int x, int y) {//快速幂
    x %= MOD;
    int res = 1;
    while (y) {
        if (y & 1) res = res * x % MOD;
        y >>= 1;
        x = x * x % MOD;
    }
    return res;
}

void init() {//预处理元和逆元
    fact[0] = infact[0] = 1;
    for (int i = 1; i < N; i++) {
        fact[i] = fact[i - 1] * i % MOD;
        infact[i] = qmi(fact[i], MOD - 2);
    }
}

int C(int a, int b) {//排列组合
    if (a < b) return 0;
    return fact[a] * infact[b] % MOD * infact[a - b] % MOD;
}

void slove()
{
    cin >> n >> m >> k;
    if (k == 1) {
        cout << 1 << endl;
        return;
    }
    int ans = 0;
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= m; j++) {
            int way = 0;
            for (int v1 = 0; v1 <= 2; v1++) {
                for (int v2 = 0; v2 <= 2; v2++) {
                    int t = C((i - v1) * (j - v2), k) % MOD * (v1 == 1 ? -2 : 1) % MOD * (v2 == 1 ? -2 : 1);//容斥定理双循环实现
                    way = ((way + t) % MOD + MOD) % MOD;
                }
            }
            ans += i * j % MOD * (n - i + 1) % MOD * (m - j + 1) % MOD * way % MOD, ans %= MOD;
        }
    }
    ans = ans * qmi(C(n * m, k), MOD - 2) % MOD;
    cout << ans << endl;
}

signed main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    init();
    int t = 1;
    while (t--) slove();
    return 0;
}

6.30

个人赛补题:

I 硬币翻面

给定串字符串,由0和1组成,可以挑s[i]与sj进行取反,求最少要多少次把全部变为0,如果变不了就输出-1

😅0110,先变成1100,再变成1001,再变成0000,三步也能变出来,卡在这里了

#include<bits/stdc++.h>
using namespace std;
int main()
{
    int t;
    cin >> t;
    while (t--)
    {
        int ll;
        cin >> ll;

        string s;
        cin >> s;
        ll = s.size();
        int t = 0;
        int dis = 0;
        for (int i = 0; i < s.size(); i++)
        {
            int w = -1;
            if (s[i] != '0')
            {
            t++;
            }
        }
        int l = -1; 
        int r = -1;
        if (t == 2)
        {

            for (int i = 0; i < s.size(); i++)
            {
                if (s[i] == '1' && l == -1)l = i;
                else if(s[i]=='1'&&l!=-1) r = i;
            }
            dis = r-l;
        }
        //cout <<t<<"  " << l << "  " << r << "  " << dis << endl;
        if (t % 2 == 1||(t==2&&dis==1&&ll==3))
        {
            cout << -1 << endl;

        }
        else if (t == 2 && ll == 4 && l == 1 && r == 2)
        {
            cout << 3 << endl;
        }
        else if (t == 2 &&ll >3&&dis==1)
        {
            cout << 2 << endl;
        }

        else if (t == 2 && ll > 3 && dis > 1)
        {
            cout << t / 2  << endl;
        }
        else
        {
            cout <<t/2 << endl;

        }
    }
}

C - Coverage

大意:给定m个集合s,每个集合中有1~n的部分数,每次可以选取其中若干个集合s,判断所选集合组成的集合能否完全覆盖1~n个数。

做到这题时,一直在考虑有没有美妙的优化做法😅,其实数据范围很小,使用dfs就可以。

#include <bits/stdc++.h>
#include<set>
using namespace std;
set<int> se[10 + 2];
int n, m, k;
int ans = 0;
void dfs(int u, set <int>check, bool flag)
{
    if (u > m)return;
    if (flag)
    {
        for (auto it : se[u])
        {
            check.insert(it);
        }
    }
    if (u == m && check.size() == n)ans++;
    dfs(u+1, check, true);
    dfs(u + 1, check, false);
}
int main()
{
    cin >> n >> m;
    for (int i = 1; i <= m; i++)
    {
        int l;
        cin >> l;
        for (int j = 1; j <= l; j++)
        {
            int k;
            cin >> k;
            se[i].insert(k);
        }

    }
    set<int>check;
    dfs(1, check, 0);
    dfs(1,check, 1);
    cout << ans << endl;
}

动态规划状态压缩

状压DP的一个重要特点,就是你看不出来他是状态压缩。这题之所以可以看作状态压缩,是因为我们可以采用0和1来表示每一行每一列的状态。

考虑放置的种类数,在横向12的矩阵放置完毕后,其实纵向21的矩阵的所有放置方式就已经固定了,因此我们只需要判定有多少种合法的横置矩阵放置数。

合法,指放完横置矩阵后,纵向仍有空间放置竖直矩阵。那么判断是否合法的依据,是当前的状态。

我们用0表示当前列此行无横置矩阵或将有横置矩阵在此列伸出,1表示上一的横置矩阵伸出到此列

合法的判断依据:j&j-1=0;(即相邻两列状态不相悖)且连续0个的个数不为奇数

还有一个需要注意的点:f[0][0]=1;可以理解为一个横置的也不放,也是一种合法方案,但是剩余的f[0][i],肯定不合法,所以都为0;

#include<bits/stdc++.h>
using namespace std;
long long int dp[13][10000];
int n,m;
bool st[10000];
int main()
{   

    while(cin>>n>>m&&(n||m))
    {  for(int i=0;i<1<<n;i++)
    {
        st[i]=1;
        int cnt=0;
        for(int j=0;j<n;j++)
        {
            if(i>>j &1)
            {
                if(cnt&1)
                {
                    st[i]=0;
                    break;
                }
            }
            else cnt++;
        }
        if(cnt&1)
                {
                    st[i]=0;

                }
    }
        memset(dp, 0, sizeof dp);
        dp[0][0]=1;
        for(int i=1;i<=m;i++)
        {
            for(int j=0;j< 1<<n;j++)
            {
                for(int k=0;k< 1<<n;k++)
                {
                    if(st[j|k]&&(j&k)==0)dp[i][j]+=dp[i-1][k];
                }
            }
        }
        cout<<dp[m][0]<<endl;
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值