A 小天才的乘法
三个范围,三个做法
第一个数据就是样例,属于不需要思考就能直接拿分的。
第二个数据有100位,可以用c++模拟高精度乘法来做,也可以用java的大数计算/python来逃课
第三个数据范围是2e6,n^2的模拟和java大数,python都无法处理,所以需要用FFT(快速傅里叶变换)来计算,其时间复杂度是O(nlogn)
FFT的模板是用来计算多项式相乘的,这里把乘数的每一位转换成多项式的系数,就能套用FFT的模板运算
代码:
#include <iostream>
#include <cstring>
#include <algorithm>
#include <cmath>
using namespace std;
const int N = 5000000;
const double PI = acos(-1);
struct Complex
{
double x, y;
Complex operator+ (const Complex& t) const
{
return {x + t.x, y + t.y};
}
Complex operator- (const Complex& t) const
{
return {x - t.x, y - t.y};
}
Complex operator* (const Complex& t) const
{
return {x * t.x - y * t.y, x * t.y + y * t.x};
}
}a[N], b[N];
char s1[N], s2[N];
int res[N];
int rev[N], bit, tot;
void fft(Complex a[], int inv)
{
for (int i = 0; i < tot; i ++ )
if (i < rev[i])
swap(a[i], a[rev[i]]);
for (int mid = 1; mid < tot; mid *= 2)
{
auto w1 = Complex({cos(PI / mid), inv * sin(PI / mid)});
for (int i = 0; i < tot; i += mid * 2)
{
auto wk = Complex({1, 0});
for (int j = 0; j < mid; j ++, wk = wk * w1)
{
auto x = a[i + j], y = wk * a[i + j + mid];
a[i + j] = x + y, a[i + j + mid] = x - y;
}
}
}
}
int main()
{
scanf("%s%s", s1, s2);
int n = strlen(s1) - 1, m = strlen(s2) - 1;
for (int i = 0; i <= n; i ++ ) a[i].x = s1[n - i] - '0';
for (int i = 0; i <= m; i ++ ) b[i].x = s2[m - i] - '0';
while ((1 << bit) < n + m + 1) bit ++ ;
tot = 1 << bit;
for (int i = 0; i < tot; i ++ )
rev[i] = ((rev[i >> 1] >> 1)) | ((i & 1) << (bit - 1));
fft(a, 1), fft(b, 1);
for (int i = 0; i < tot; i ++ ) a[i] = a[i] * b[i];
fft(a, -1);
int k = 0;
for (int i = 0, t = 0; i < tot || t; i ++ )
{
t += a[i].x / tot + 0.5;
res[k ++ ] = t % 10;
t /= 10;
}
while (k > 1 && !res[k - 1]) k -- ;
for (int i = k - 1; i >= 0; i -- ) printf("%d", res[i]);
return 0;
}
B GCW的解密之旅
数据范围有点迷惑性,可能会有人试图暴搜(事实上都没什么人开这道题,诡异)。其实题目中说了每个柱子的编号必定小于其关联柱的编号。这意味着能改变某柱状态的柱子只能是它自己或其左边的柱子。所以我们只需要从左往右遍历一遍,如果这个柱子是反面就翻转它,如果是正面就不管。每次翻转都要检查一下所有柱子的状态及时break,以保证翻转次数是最少的
#include <iostream>
#include <vector>
using namespace std;
const int N = 30;
int st[N];
vector<int>g[N];
int ok, ans;
int n;
bool check() {
for (int i = 1; i <= n; i++) {
if (!st[i]) {
return false;
}
}
return true;
}
void turn(int x) {
st[x] ^= 1;
for (int i = 0; i < g[x].size(); i++) {
st[g[x][i]] ^= 1;
}
}
int main() {
cin >> n;
for (int i = 1; i <= n; i++)
cin >> st[i];
for (int i = 1; i <= n; i++) {
int k;
cin >> k;
for (int j = 1; j <= k; j++) {
int num;
cin >> num;
g[i].push_back(num);
}
}
for (int i = 1; i <= n; i++) {
if (check()) {
ok = 1;
break;
}
if (!st[i]) {
ans++;
turn(i);
}
}
if (ok) {
cout << ans << endl;
} else {
cout << "-1" << endl;
}
return 0;
}
C 简单数据结构
本题的数据范围是按操作方式来区分,33%的数据只有查询,所以可以用前缀和拿到第一个数据的分数。66%数据有查询和单点修改,这就可以套一个线段树的模板,当然也可以用带修改莫队来解决,莫队比线段树更方便调试,一般代码也更短。全部数据有查询,单点修改,区间修改。可以用线段树+懒标记来解决:线段树
(好像有人用前缀和莽过了第二个数据,就很诡异)
代码:
#include <iostream>
#include <iostream>
#include <fstream>
using namespace std;
typedef long long ll;
const int N = 100010;
ll w[N];
struct node {
int l, r;
ll sum, add;//add就用来储存懒标记的修改
} tr[4 * N];
void pushup(int u) {
tr[u].sum = tr[u << 1].sum + tr[u << 1 | 1].sum;
}
void pushdown(int u) {//与pushup向上传递信息相反,pushdown是向下传递懒标记
auto &root = tr[u], &left = tr[u << 1], &right = tr[u << 1 | 1];
//用root,left,right分别指代父节点,左右儿子。简化书写
if (root.add) {//如果父节点中存在懒标记
left.add += root.add;//让左儿子继承父亲的懒标记
left.sum += (left.r - left.l + 1) * root.add;//左儿子值要进行修改
//因为父亲传递到左儿子的add是针对区间l到r中一共(r-l+1)个数的,left.sum就要加上所有数的增量
right.add += root.add;//右儿子同理
right.sum += (right.r - right.l + 1) * root.add;
root.add = 0;//父亲的懒标记传递完了,清空
}
}
void build(int u, int l, int r) {
if (l == r) {
tr[u] = {l, r, w[r], 0};
} else {
tr[u] = {l, r};
int mid = l + r >> 1;
build(u << 1, l, mid);
build(u << 1 | 1, mid + 1, r);
pushup(u);
}
}
void modify(int u, int l, int r, int d) {
if (tr[u].l >= l && tr[u].r <= r) {//如果修改区间覆盖了整个范围,就只需要打一个懒标记就好
tr[u].sum += (ll)(tr[u].r - tr[u].l + 1) * d;
tr[u].add += d;
} else {
pushdown(u);//没有完全包括这个区间,这时候就需要对值进行修改,修改前需要先把懒标记传下去
int mid = tr[u].l + tr[u].r >> 1;
if (l <= mid)
modify(u << 1, l, r, d);
if (r > mid)
modify(u << 1 | 1, l, r, d);
pushup(u);
}
}
ll query(int u, int l, int r) {
if (tr[u].l >= l && tr[u].r <= r)
return tr[u].sum;
pushdown(u);//同样是再询问前将懒标记传下去
ll v = 0;
int mid = tr[u].l + tr[u].r >> 1;
if (l <= mid)
v = query(u << 1, l, r);
if (r > mid)
v += query(u << 1 | 1, l, r);
return v;
}
int main() {
int n, m;
int a, b, c;
int ch;
cin >> n >> m;
for (int i = 1; i <= n; i++)
cin >> w[i];
build(1, 1, n);
while (m--) {
cin >> ch ;
if (ch == 1) {
cin >> a >> b;
cout << query(1, a, b) << endl;
} else if (ch == 2) {
cin >> a >> b;
modify(1, a, a, b);
} else {
cin >> a >> b >> c;
modify(1, a, b, c);
}
}
}
D 重生之我是grineer之Sentient一刀一个
这题出锅了,本来是想搞一个网络流的题,结果贪心一下就出来了QWQ
一个模板题,只是稍微加了一点点修饰。将所有陆战队与低于其战斗力的登陆舰之间建边,然后就是一个二分图最大匹配问题。
对于第一个数据可以用匈牙利算法解决,但第二个数据会因为递归层数太多导致RE,所以需要用到网络流最大流求解:创建一个超级源点与所有陆战队相连,创建一个超级汇点与所有登陆舰相连,然后再跑一遍网络最大流的模板就好。
(事实上贪心一下就过了QWQ,但我们做题是为了学算法的)
代码:
#include <iostream>
#include <cstring>
#include <algorithm>
#include <fstream>
using namespace std;
const int N = 100010, M = 6000100, inf = 1e8;
int n, m, S, T;
int h[M], e[M], f[M], ne[M], idx;
//h 表头 e边的终点 f边的权值
int q[M], d[M], cur[M];
int a[M], b[M];
void add(int a, int b, int c) {
e[idx] = b, f[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
e[idx] = a, f[idx] = 0, ne[idx] = h[b], h[b] = idx ++ ;
}
bool bfs() {
int hh = 0, tt = 0;
memset(d, -1, sizeof d);
q[0] = S, d[S] = 0, cur[S] = h[S];
while (hh <= tt) {
int t = q[hh ++ ];
for (int i = h[t]; ~i; i = ne[i]) {
int ver = e[i];
if (d[ver] == -1 && f[i]) {
d[ver] = d[t] + 1;//ver是t的下一级,防止回流
cur[ver] = h[ver];
if (ver == T)
return true;
q[ ++ tt] = ver;
}
}
}
return false;
}
int find(int u, int limit) {
if (u == T)
return limit;//到了最终点,不用考虑后面的,上限就是可扩展的量
int flow = 0;
for (int i = cur[u]; ~i && flow < limit; i = ne[i]) {
cur[u] = i; //当前弧优化
int ver = e[i];
if (d[ver] == d[u] + 1 && f[i]) {
int t = find(ver, min(f[i], limit - flow)); //下一条流量的最小值
//或者当前流量限制减去这条路已经花掉的流量中的最小值
if (!t)
d[ver] = -1; //遍历过了
f[i] -= t;
f[i ^ 1] += t;
flow += t;
}
}
return flow;//返回可拓展流量
}
int dinic() {
int r = 0, flow;
while (bfs())//用bfs划分阶级
while (flow = find(S, inf))//不断用dfs进行扩展
r += flow;
return r;
}
int main() {
cin >> m >> n ;
for (int i = 1; i <= m; i++) {
cin >> a[i];
}
for (int j = 1; j <= n; j++) {
cin >> b[j];
}
sort(a + 1, a + 1 + m);
sort(b + 1, b + 1 + n);
//20000
//创建一个超级源点S和一个超级汇点T,将问题转化为从
//s开始,T结束的网络流
S = 0, T = n + m + 2 ;
memset(h, -1, sizeof h);
for (int i = 1; i <= m; i++)
add(S, i, 1);
for (int i = m + 1; i <= n + m + 1; i++)
add(i, T, 1);
for (int i = m; i >= 1; i--) {
//cout << i << endl;
for (int j = 1; j <= n; j++) {
if (a[i] > b[j]) {
add(i, j + m + 1, 1);
//cout << i << " " << j << endl;
} else
break;
}
}
cout << dinic() << endl;
return 0;
//cout << dinic() << endl;//输出最大流(即最大的匹配数)
}
E (Extras)Digital Matrix
最开始没这道题,前两天在Y总的每日一题里碰到的,感觉很不错,就翻成英文后放这了(它原本是cf上的一道题)
大意就是说:给一个矩阵,矩阵中的数有正有负。每次可以选择两个相邻的数,将这两个数乘-1,求这样操作后整个矩阵的和的最大值
实际上任意选取矩阵上的两个点,都可以在不影响其他点的情况下将这两个点乘-1
比如选择(1,0)和(4,0),先后选择(1,0)(2,0), (2,0)(3,0) ,(3,0)(4,0)。这样就可以在不影响中间的数的情况下,给(1,0)(4,0)乘上-1。
所以我们可以选择两个负数操作。如果负数数量为偶数,就可以把全部负数变成正数。如果数量为奇数,就只留下绝对值最小的负数不变,这样就能保证总和最大。
代码:
#include <iostream>
using namespace std;
int n, m;
int read() {
int a;
cin >> a;
return a;
}
int main() {
int t = read();
while (t--) {
cin >> n >> m;
int cnt = 0;
int minn = 1e8;
int ans = 0;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
int a = read();
ans += abs(a);
minn = min(minn, abs(a));
if (a < 0)
cnt++;
}
}
if (cnt % 2) {
cout << ans - 2 * minn << endl;
} else {
cout << ans << endl;
}
}
}
F 黑白三角形
提示已经给得足够多了,题目的难点已经从思维题和推式子转到了如何求出两种角分别有多少种。
枚举所有的边,对于每次循环枚举出的边,能组成不同色角的边有黑边数*白边数 ,而组成同色角的数量是从黑/白色中任选不同两边的组合方案数。
#include <iostream>
#include <cstring>
#include <algorithm>
#include <vector>
namespace std {
unsigned z1, z2, z3, z4, b, u;
unsigned get() {
b = ((z1 << 6)^z1) >> 13;
z1 = ((z1 & 4294967294U) << 18)^b;
b = ((z2 << 2)^z2) >> 27;
z2 = ((z2 & 4294967288U) << 2)^b;
b = ((z3 << 13)^z3) >> 21;
z3 = ((z3 & 4294967280U) << 7)^b;
b = ((z4 << 3)^z4) >> 12;
z4 = ((z4 & 4294967168U) << 13)^b;
return (z1 ^ z2 ^ z3 ^ z4);
}
bool read() {
while (!u)
u = get();
bool res = u & 1;
u >>= 1;
return res;
}
void srand(int x) {
z1 = x;
z2 = (~x) ^ 0x233333333U;
z3 = x ^ 0x1234598766U;
z4 = (~x) + 51;
u = 0;
}
}
using namespace std;
bool edge[8005][8005];
vector<int>g[8005];
vector<int>g1[8005];
int f[8005];
int find(int x) {
if (x != f[x])
f[x] = find(x);
return f[x];
}
int main() {
int n, seed;
cin >> n >> seed;
srand(seed);
for (int i = 1; i <= n; i++)
for (int j = i + 1; j <= n; j++)
edge[j][i] = edge[i][j] = read();
long long a = 0, b = 0;
for (int i = 1; i <= n; i++) {
long long c0 = 0, c1 = 0;
for (int j = 1; j <= n; j++) {
if (i == j)
continue;
if (edge[i][j]) {
c1++;//分别求两种边的数量
} else
c0++;
}
a += (long long) c1 * c0; //两边不同的角的数量
b += (long long) (c1 * (c1 - 1) / 2) + (c0 * (c0 - 1) / 2); //两边相同相同的角的数量
}
cout << (long long)(b - a / 2) / 3 << endl;
G 巴洛·基·提尔的特惠商品
非典型图论题,最小生成树
来自牛客多校赛第三场B题,把描述修改了一下
题解我尝试写了一会,感觉怎么都表述不清楚,所以这里只好用底味爷的题解(我菜死了)
dev爷的博客
==================================
2*2小矩阵染三另一免费
这是唯一一个约束,那易想到一个万能解法,那就是选一列全选了,然后之后每一列都选一个点,剩下的点都是免费的,那么一共选了n+m-1个点;显然这个并不是最优解,因为没有考虑点权。
继续yy:
(i,j),(i+1,j),(i,j+1)都选了,
那么第四个点就能取了,考虑性质连通性,显然可以建立行列联通思想.
于是 定义(i,j)表示存在一条i-j的边,权值为点权; 至此豁然开朗,那么为什么i+1和j+1免费了,因为第i+1行和第i行联通,第i行和第j列联通,第j列和第j+1列联通。
大为震撼,现在验算一下:
如果选了若干点,但是不满足上面的条件能免费吗,显然是不能的,最后得出来就是把n行和m列全部勾连起来,剩下的点全部全部全部全部免费!
所以:这个题就是有n+m个点,有n*m条边,求MST,做完了;
原文链接:https://blog.csdn.net/DevourPower/article/details/119063056
==========================================
代码
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
const int N = 5010;
typedef pair<int, int> pii;
typedef long long ll;
struct node {
int x, y; //这个节点的位置
int v;//这个节点的权值
} no[N * N];
int f[N * 2]; //并查集
int find(int x) {
if (x != f[x])
f[x] = find(f[x]);
return f[x];
}
void add(int x, int y) {
x = find(x), y = find(y);
f[x] = y;
}
ll ans;//储存答案
int cnt;
vector<pii>g[100010];//g[i]表示权值为i的点有哪些
int main() {
int n, m, a, b, c, d, p;
cin >> n >> m >> a >> b >> c >> d >> p;
for (int i = 1; i <= n + m; i++) {
f[i] = i;
}
no[0].v = a;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
cnt++;
no[cnt].x = i;
no[cnt].y = n + j; //编号不能重复
no[cnt].v = ((ll)no[cnt - 1].v * no[cnt - 1].v * b + (ll)no[cnt - 1].v * c + d) % p;
g[no[cnt].v].push_back({i, n + j});
}
}
int tot = 0;
for (int i = 0; i < p; i++) {
for (int j = 0; j < g[i].size(); j++) {
if (find(g[i][j].first) != find(g[i][j].second)) {
ans += i;
add(g[i][j].first, g[i][j].second);
tot++;
if (tot == n + m - 1)
break;
}
}
if (tot == n + m - 1)
break;
}
cout << ans << endl;
}