本周学习的东西:
-
[ ] 概率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;
}
}