26ICPC成都 DP|预处理|计数|通信|欧拉序|二分|贪心|维护区间
C
计数 DP 预处理 枚举
一个长度100的01序列b,做前缀和得到a,对a做前缀和得到s。现在给m个约束p,v,表示s[p]=vs[p]=vs[p]=v。问满足所有约束的b序列的个数
对于俩相邻的约束(i,vi)(j,vj)(i,v_i)(j,v_j)(i,vi)(j,vj),有sj−si=ai∗(j−i)+x=vj−vis_j-s_i=a_i*(j-i)+x=v_j-v_isj−si=ai∗(j−i)+x=vj−vi
于是可以枚举aia_iai,如果aia_iai确定了,xxx就确定了。只需要求出这个x的方案数即可计数了。
注意到对于一个位置iii,所有[0,100][0,100][0,100]的aia_iai的方案数可能不同,所以我们还需要维护到达iii位置,不同的aia_iai的方案数,再乘上上面的x的方案数,才能计数。
具体来说我们考虑一个dpxdp_xdpx,表示当前前缀约束都满足后,当前位置ai=xa_i=xai=x的方案数。
转移时,对于俩相邻的约束(i,vi)(j,vj)(i,v_i)(j,v_j)(i,vi)(j,vj),枚举所有aia_iai,可以转移到所有aja_jaj。转移系数是(i+1,j)(i+1,j)(i+1,j)这区间内,b序列和为aj−aia_j-a_iaj−ai,s增加量为vj−viv_j-v_ivj−vi的方案数,记为f。根据我们前面的分析,这一段对前缀和的贡献是ai∗(j−i)+xa_i*(j-i)+xai∗(j−i)+x,其中x就是这一段(i+1,j)(i+1,j)(i+1,j)序列单独算前缀和的结果。
f显然可以dp预处理。dp状态里维护01序列长度,一阶前缀和a,二阶前缀和s的值。dp值维度维护方案数
最后一个约束如果不为n,那么到n这一段都可以任取,乘上一个2x2^x2x即可
这里有一个复杂度分析,任意两个相邻约束之间的转移,需要枚举aia_iai,枚举这一段01序列中1的个数,这样的约束有m≤100m\le100m≤100个,看起来是个1003100^31003。
但其实考虑m个约束,和枚举相邻两个约束之间1的个数,这两层循环,总次数只有最多100次,因此这个dp计算量只有1002100^21002,对于T=1e4T=1e4T=1e4可以通过
#include <bits/stdc++.h>
using namespace std;
#define int long long
#define rep(i, x, y) for (int i = (x); i <= (y); i++)
const int inf = LONG_LONG_MAX;
const int mod = 998244353;
int qpow(int a, int b)
{
int res = 1;
while (b)
{
if (b & 1)
{
res = res * a % mod;
}
a = a * a % mod;
b >>= 1;
}
return res;
}
int f[110][110][5500];
void pre()
{
f[0][0][0] = 1;
rep(i, 0, 100)
{
rep(j, 0, i)
{
rep(k, 0, 5100)
{
f[i + 1][j + 1][k + j + 1] += f[i][j][k];
f[i + 1][j + 1][k + j + 1] %= mod;
f[i + 1][j][k + j] += f[i][j][k];
f[i + 1][j][k + j] %= mod;
}
}
}
}
void solve()
{
int n, m;
cin >> n >> m;
int pre = 1, prev = 0;
vector<int> g(2 * n + 10);
g[0] = 1;
rep(i, 1, m)
{
int p, v;
cin >> p >> v;
int len = p - pre;
vector<int> ng(2 * n + 10);
// a[pi-1]
rep(j, 0, pre)
{
int V = v - prev - j * len;
if (V < 0 || V > 5100)
{
continue;
}
// a[pi-1,pi]
rep(k, 0, len)
{
// cout << j << ' ' << k << ' ' << g[j] << ' ' << V << ' ' << f[len][k][V] << '\n';
ng[j + k] += g[j] * f[len][k][V] % mod;
}
}
rep(j, 0, p)
{
ng[j] %= mod;
}
swap(ng, g);
pre = p;
prev = v;
}
int ans = 0;
rep(i, 0, n)
{
ans += g[i];
}
// cout << ans << ' ';
ans %= mod;
if (pre < n)
{
int t = qpow(2, n - pre);
ans = ans * t % mod;
}
cout << ans << '\n';
}
signed main()
{
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
int T = 1;
cin >> T;
pre();
while (T--)
{
solve();
}
return 0;
}
/*
6
1 0
3 1
3 1
3 1
2 0
4 1
4 3
5 2
1 0
5 4
5 2
2 0
5 10
*/
D
签到 转化
俩人各自有个血量和攻击,每次对撞,各自的血量扣除对方的攻击。并且有4种情况,可能给扣血都乘上相同的系数1,2,4。A赢只有B死了A活着,否则都算B赢。给出初始的血量和攻击,问谁赢
注意到每个人扣得血只能是对方攻击力的倍数,于是不妨给每个的血量除以对方的攻击力,考虑到不足一次攻击的血量,也需要一次攻击打掉,这里上取整。接下来每次对撞,就是俩人血量都同时扣1,2,4了。
这是简单的,a想要稳赢,需要至少比b多4点血,最极端的情况是碰之前5,1,碰完4,0。
void solve()
{
int a, b, ha, hb;
cin >> a >> b >> ha >> hb;
int x = (ha + b - 1) / b;
int y = (hb + a - 1) / a;
if (x - y >= 4)
{
cout << "yes\n";
}
else
{
cout << "no\n";
}
}
I
通信 欧拉序
给一个森林,n个点,你可以在树上每个节点上,传输一个信息,形成一个长度n的数组a。然后通信过程中会把这个a数组随机打乱得到b。接收方收到b后,需要还原原始的森林。还原森林,每条边的顶点下标不需要一样,拓扑结构一样即可。a的值域可以在[1,2n][1,2n][1,2n]
开始的一个思路是传输每个点的父亲,然后发现[1,2n]这个条件没用完全,于是考虑森林里每棵树的根额外+n,这样接收方可以通过>n\gt n>n确定哪些是根
但这样有个解决不了的问题:一个父亲的多个儿子,他们传输的信息是一样的,再经过打乱后,我们完全区分不了这些点的顺序。但是题目的要求其实是需要区分的,于是这个编码方式宣告失败。实际上,任何可能有两个aia_iai相同的编码方式,都会有这个问题,都是不行的
从值域角度分析,[1,2n][1,2n][1,2n]可以联想到欧拉序。即使不从这个角度,从严谨的方向分析,欧拉序其实保存了一个树上dfs的操作序列,每次in操作等于往下进入一个儿子,一次out操作等于往上返回父亲。这里我们传递不了长度2n的序列,只能传递长度n。
那么考虑传递每个点第一次进入的时刻,也就是inxin_xinx。接收端,把b排序,对于两个bi−1,bib_{i-1},b_ibi−1,bi,记录x=bi−bi−1x=b_i-b_{i-1}x=bi−bi−1,那么每到一个bib_ibi,往上跳父亲x步,再往下走一步,到一个儿子。可以记录所有bib_ibi值在b数组中对应的下标,每次向下走一步,到了一个儿子,就是父亲和儿子的下标之间连一条边。
对比排序有个更简单的做法是开个2n2n2n的值域数组,记录哪些值出现了,然后扫一遍[1,2n][1,2n][1,2n],如果一个位置是1则往下到一个儿子,否则往上退一步父亲。时刻维护当前在哪个点,以及已经走过的每个点的父亲(用于回退)。每次往下走时,就确定了一条边。
这第个做法其实就是利用欧拉序,在模拟dfs过程,于是自然就能还原原始森林的拓扑。欧拉序告诉了我们每一步应该往下走儿子,还是往上回父亲。虽然只能传n个往下走儿子的操作,但是只有两种操作,且一共2n个操作,那么[1,2n]里剩下的操作自然就是往上回到父亲。
#include <bits/stdc++.h>
using namespace std;
#define int long long
#define rep(i, x, y) for (int i = (x); i <= (y); i++)
const int inf = LONG_LONG_MAX;
const int mod = 998244353;
signed main()
{
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
string s;
cin >> s;
int T = 1;
cin >> T;
while (T--)
{
if (s == "cat")
{
int n, m;
cin >> n >> m;
vector<vector<int>> g(n + 10);
rep(i, 1, m)
{
int u, v;
cin >> u >> v;
g[u].push_back(v);
g[v].push_back(u);
}
vector<int> in(n + 10), out(n + 10);
int cnt = 0;
auto &&dfs = [&](auto &&dfs, int u, int f) -> void
{
in[u] = ++cnt;
for (int v : g[u])
{
if (f == v)
continue;
dfs(dfs, v, u);
}
out[u] = ++cnt;
};
rep(i, 1, n)
{
if (!in[i])
{
dfs(dfs, i, 0);
}
}
rep(i, 1, n)
{
cout << in[i] << ' ';
}
cout << '\n';
}
else
{
int n;
cin >> n;
vector<int> b(n + 10), mp(2 * n + 10), vis(2 * n + 10), f(n + 10);
rep(i, 1, n)
{
cin >> b[i];
mp[b[i]] = i;
vis[b[i]] = 1;
}
vector<pair<int, int>> ans;
int now = 0;
rep(i, 1, 2 * n)
{
int tmp = mp[i];
if (vis[i])
{
if (now != 0)
{
ans.push_back({now, tmp});
}
f[tmp] = now;
now = tmp;
}
else
{
now = f[now];
}
}
cout << ans.size() << '\n';
for (auto [u, v] : ans)
{
cout << u << ' ' << v << '\n';
}
}
}
return 0;
}
/*
cat
4
1 0
4 3
1 2
4 3
2 3
7 2
6 4
7 1
10 7
10 9
7 9
10 5
4 8
3 7
4 10
1 2
*/
K
二分 贪心 思维
一个序列a,一个序列l。现在需要确定一个序列b,满足∣bi−bi−1∣≥li|b_i-b_{i-1}|\ge l_i∣bi−bi−1∣≥li,然后最小化max∣ai−bi∣\max|a_i-b_i|max∣ai−bi∣
最小化最大值,考虑二分,于是对于一个二分值E,约束了bi∈I,I=[ai−E,ai+E]b_i∈I,I=[a_i-E,a_i+E]bi∈I,I=[ai−E,ai+E]。然后在此基础上考虑lil_ili的约束。那么假设前一个bib_ibi在一个区间[L,R][L,R][L,R],那么新的bib_ibi区间就是[L+li,+∞]∪[−∞,R−li][L+l_i,+\infty]\cup[-\infty,R-l_i ][L+li,+∞]∪[−∞,R−li],再交上III,
这里看起来每一轮,上一轮的一个合法区间会分裂成最多两个,那nnn轮可能会变成O(2n)O(2^n)O(2n)个?
实则不然,如果前一轮已经有俩,那么俩一定是[L+li,ai+E]∪[ai−E,R−li][L+l_i,a_i+E]\cup[a_i-E,R-l_i ][L+li,ai+E]∪[ai−E,R−li]这样的,这一轮就会变成得到[−∞,ai+E−li+1]∪[−∞,R−li−li+1]∪[L+li+li+1,+∞]∪[ai−E+li+1,+∞][-\infty,a_i+E-l_{i+1}]\cup[-\infty,R-l_i-l_{i+1} ]\cup [L+l_i+l_{i+1},+\infty]\cup[a_i-E+l_{i+1},+\infty ][−∞,ai+E−li+1]∪[−∞,R−li−li+1]∪[L+li+li+1,+∞]∪[ai−E+li+1,+∞]
注意到这四个是两个朝左开口,两个朝右开口的,同向的可以合并,取并集一定最多只剩两个。于是只要区间有两个了,接下来最多还是只有两个。
基于此我们可以直接手动取交集,维护这两个区间。最后看合法区间是否为空,不空则check有解。也可以利用这个性质,把每轮产生的交集做个贪心的区间合并,套一下模版,也就是要对产生的区间排序一下,然后贪心合并。
#include <bits/stdc++.h>
using namespace std;
#define int long long
#define rep(i, x, y) for (int i = (x); i <= (y); i++)
const int inf = LONG_LONG_MAX / 2;
int n;
int adj[200005];
int L[200005];
bool check(int m)
{
// cout << "now m: " << m << "\n";
int l1 = adj[0] - m, r1 = adj[0] + m, l2 = adj[0] - m, r2 = adj[0] + m;
int flag = 2;
for (int i = 1; i < n; i++)
{
int x1 = max(r1 - L[i], r2 - L[i]), y1 = min(l1 + L[i], l2 + L[i]);
// cout << i << ": " << l1 << " " << r1 << " " << l2 << " " << r2 << " " << x1 << " " << y1 << " " << "\n";
if (x1 < adj[i] - m)
{
flag--;
r1 = -inf, l1 = inf;
}
else
{
r1 = min(x1, adj[i] + m);
l1 = adj[i] - m;
}
if (y1 > adj[i] + m)
{
flag--;
r2 = -inf, l2 = inf;
}
else
{
l2 = max(y1, adj[i] - m);
r2 = adj[i] + m;
}
// cout << i << ": " << l1 << " " << r1 << " " << l2 << " " << r2 << " " << x1 << " " << y1 << " " << "\n";
if (flag == 0)
{
return 0;
}
flag = 2;
}
return 1;
}
signed main()
{
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
int T;
cin >> T;
while (T--)
{
cin >> n;
int l = 0, r = 100;
for (int i = 0; i < n; i++)
{
cin >> adj[i];
r += adj[i];
}
for (int i = 1; i < n; i++)
{
cin >> L[i];
r += L[i];
}
int ans = -1;
while (r >= l)
{
int mid = (l + r) / 2;
if (check(mid))
{
ans = mid;
r = mid - 1;
}
else
{
l = mid + 1;
}
}
cout << ans << "\n";
}
return 0;
}
M
贪心
每个aia_iai可以在[li,ri][l_i,r_i][li,ri]里选。一个aia_iai严格大于前面所有的aja_jaj,则是坏位置,问坏位置最少个数。规定a1a_1a1一定是坏位置。
考虑贪心,想让判断一个位置是否是坏位置,可以维护前面选的位置最大值mx,当前选择大于mx则是一个坏位置。
如果当前区间lr的最小值也就是l,已经大于mx了,则这里肯定要增加一个坏位置了。贪心的想,如果必须要增加了,为了让后面的坏位置尽量少,肯定要选一个尽量大的。但这个尽量大的不一定是当前区间的r,还可能是上一次选择mx后,到这个区间之间的所有r。
因此
- 如果不是必须增加一个坏位置,维护候选的r的最大值
- 如果必须增加一个坏位置了,从候选r和当前r里选一个最大的。然后重置候选r为负无穷
void solve() {
int n;
cin >> n;
long long l, r;
cin >> l >> r;
int ans = 1;
long long mx = r;
long long best_r = -2e9;
for (int i = 2; i <= n; ++i) {
cin >> l >> r;
if (l > mx) {
ans++;
mx = max(best_r, r);
best_r = -2e9;
} else {
best_r = max(best_r, r);
}
}
cout << ans << "\n";
}
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)