A
结论
nnn个人,两队,每队2−32-32−3个人,问两队人数差的最小值?
<=3<=3<=3的话只能分一队,另一队空,特判。
否则,如果是偶数,则可以均分成两队,因为模444余222则可以最后六个人分成两个三,剩下的均分,模444余000则可以直接均分。
如果是奇数,在偶数分法的基础上,会有一队多一人
B
分类讨论 循环
计时sss分的沙漏,每kkk分钟颠倒一次,问mmm分钟后,如果不管了,多久后沙漏会停止流动
也就是问mmm分钟时还剩多少沙子在上面。注意到这个kkk分钟颠倒一次,其实有周期性,如果s<=ks<=ks<=k,那每kkk分钟,前sss分钟沙漏会完全流完。如果s>ks>ks>k,每2k2k2k一个周期,前kkk是sss流到s−ks-ks−k,后kkk是kkk流到000。
那根据s,ks,ks,k的大小,以及mmm模2k2k2k,余数是否大于kkk分讨即可。
C
记忆化搜索
一个nnn,每次对半分,问得到111最少需要操作几次?
对半分是折半的,变小的很快,爆搜即可。但分支很多,也就是状态数还是很多,是O(n)O(n)O(n)的,需要记忆化,记录得到每个数字的最小操作次数,如果当前分支的操作次数大于记录的最小次数,剪掉。
D
位运算 组合数学
一个ddd位的二进制数,每次可以减一,或者如果是偶数的话,可以除二,要在kkk次操作内变成000,问从多少个数字开始,kkk次操作内无法变成000。
从二进制的角度,这两种操作分别就是:把第000位的111删掉,右移一位。那么二进制表示的每一个111显然都要一次,并且最高位的111也要右移到第000位才能删除,所以最高位是mxmxmx的话,还要右移mxmxmx次。
现在要计算操作次数大于kkk的元素个数,肯定无法枚举ddd位的所有数。考虑枚举最高位,这样右移次数是确定的,并且最高位一定有个111,需要用一次删除,然后要让删除操作和右移次数加起来超过kkk,那么删除次数至少是k−mx−1k-mx-1k−mx−1,也就是要安排这么多个111在[0,mx−1][0,mx-1][0,mx−1]这些位上,这些位置一共mxmxmx个空位,最多安排mxmxmx个一,所以111的个数取值范围是[k−mx−1,mx−1][k-mx-1,mx-1][k−mx−1,mx−1],枚举111的个数xxx,方案数就是C(mx,x)C(mx,x)C(mx,x)
c
void solve() {
int n, k;
cin >> n >> k;
int mx = __lg(n);
int ans = 0;
rep(i, 1, mx - 1) {
int cost = i + 1;
rep(j, k - cost + 1, i) {
ans += C(i, j);
}
}
if (mx + 1 > k)ans++;
cout << ans << '\n';
}
E
set维护区间
一个排列,对于i∈[1,n−1]i∈[1,n-1]i∈[1,n−1],问相邻元素差不小于iii的子数组个数。
子数组太多,不能直接统计。考虑一个iii的所有区间是什么样的,其实就是所有小于iii的ai−ai−1a_i-a_{i-1}ai−ai−1的位置都断开,形成若干个区间,然后这些区间内的子数组都合法。对于一个区间[l,r][l,r][l,r]子数组个数是(r−l+1)(r−l)/2(r-l+1)(r-l)/2(r−l+1)(r−l)/2
注意iii变大过程中,断开位置是在之前基础上不断变多的,因此可以从小到大枚举iii,逐步增加断开位置,更新答案的变化量。对于一个iii我们要保存所有ai−ai−1a_i-a_{i-1}ai−ai−1的位置,然后把这些位置所在的区间断开。断开时先撤销区间的答案,然后加上断开后两个新区间的答案。
注意到这个过程也可以反着来,就是区间并查集。同样也可以做。
c
int cal(int l, int r) {
return (r - l + 1) * (r - l) / 2;
}
void solve() {
int n;
cin >> n;
vi a(n + 1);
vvi pos(n + 1);
int cur = cal(1, n);
rep(i, 1, n) {
cin >> a[i];
if (i != 1) {
pos[abs(a[i] - a[i - 1])].push_back(i);
}
}
set<pii>s;
s.insert({1, n});
for (int i = 1; i < n; i++) {
cout<<cur<<' ';
for (int p : pos[i]) {
auto it = s.lower_bound({p, p});
--it;
auto [l, r] = *it;
s.erase(it);
cur -= cal(l, r);
s.insert({l, p - 1});
s.insert({p, r});
cur += cal(l, p - 1);
cur += cal(p, r);
}
}
cout << '\n';
}
F
树形背包
选若干个不相交子树,覆盖所有叶子,问选择次数能否为三的倍数?
选择次数是三的倍数,就是模333余零,可以用一个取模背包解决,所以这实际上是一个树形背包问题,每个点枚举儿子为根的子树选不选。由于模数很小,暴力复杂度很低,不需要一般树形背包的优化。
实现上有两个问题:第一是只能选不相交子树,也就是如果我们选了xxx子树,就不能选xxx子树内后代为根的子树。这一点在实现上,需要让选当前xxx的转移,和选儿子的转移时一个或的关系,也就是在儿子的转移结束后,单独进行选整个xxx子树的转移
另一点是,我们这个dpdpdp状态里,000可能是选了000个子树的初始状态,从这个角度讲应该初始化成dp0=1dp_0=1dp0=1,但也可能表示选了3k3k3k个子树。那么如果就可能出现,实际上选了3k3k3k个的情况并不存在,初始化dp0=1dp_0=1dp0=1,会让我们误认为3k3k3k的情况是可行的。
这需要我们不能无条件初始化,只有需要利用dp0dp_0dp0这个状态进行转移前,才能初始化dp0=1dp_0=1dp0=1,这样转移后dp0=1dp_0=1dp0=1一定意味着存在一个选3k3k3k个子树的方案。
c
void solve() {
int n;
cin >> n;
vvi g(n + 1);
rep(i, 1, n - 1) {
int u, v;
cin >> u >> v;
g[u].push_back(v);
g[v].push_back(u);
}
vvi h(n + 1, vi(3));
auto &&dfs = [&](auto &&dfs, int u, int f)->void{
bool c = 0;
for (int v : g[u]) {
if (v == f)continue;
dfs(dfs, v, u);
vi nh(3);
if (!c) {
c = 1;
h[u][0] = 1;
}
rep(i, 0, 2) {
rep(j, 0, 2) {
if (h[u][i] == 1 && h[v][j] == 1)
nh[(i + j) % 3] |= 1;
}
}
// rep(i,0,2){
// cout<<h[u][i]<<' ';
// }
swap(nh, h[u]);
}
h[u][1] = 1;
};
dfs(dfs, 1, -1);
if (h[1][0] == 1) {
cout << "yes\n";
} else {
cout << "no\n";
}
}
G
线段树二分
带修,问一个区间[l,r][l,r][l,r]是否存在ddd,满足min([l,l+d])=dmin([l,l+d])=dmin([l,l+d])=d
注意到min([l,l+d])min([l,l+d])min([l,l+d])不增,ddd单增,所以他们相等的点一个,并且不一定是整数,显然只有整数才有解。所以问题可以转化为d−min([l,l+d])d-min([l,l+d])d−min([l,l+d])这个单增的函数,在[l,r][l,r][l,r]内是否存在整点的零点
线段树二分即可。注意这不是[1,n][1,n][1,n]的线段树二分,而是部分区间的二分,需要特殊写法。仍然把区间拆成O(logn)O(log n)O(logn)个区间,然后我们的递归优先访问左儿子,相当于从左到右访问这些区间,然后把它们依次合并,如果过合并后,d−min([l,l+d])d-min([l,l+d])d−min([l,l+d])仍小于零,由于这是个单增函数,说明零点还在右边,访问右儿子。如果大于等于零了,说明零点就在这个区间内,继续向下递归,指代递归到一个叶子。
这个过程可以抽象为,我们要找到一个[l,i][l,i][l,i]这个区间都满足某个条件的,最右的iii,因此一般我们需要在递归过程中传一个引用参数,或者设置一个全局变量,维护我们目前访问到过[l,i][l,i][l,i]的结果,每次访问到一个新的区间,如果整个区间都合并了,仍然满足条件,则把当前区间合并到[l,i][l,i][l,i]内。
对于本题的话,我们就是要找到满足d−min([l,l+d])<0d-min([l,l+d])<0d−min([l,l+d])<0的最大的iii,或者d−min([l,l+d])>=0d-min([l,l+d])>=0d−min([l,l+d])>=0的最小的iii,那么维护一个[l,i][l,i][l,i]的最小值,然后用这个最小值计算d−min([l,l+d])d-min([l,l+d])d−min([l,l+d]),判断正负,作为是否递归的条件。
得到iii后,这可能已经是d−min([l,l+d])>0d-min([l,l+d])>0d−min([l,l+d])>0的点了,并不是零点,我们关心的是零点是不是整数,所以还需要检查这一点的d−min([l,l+d])d-min([l,l+d])d−min([l,l+d])值是否等于零。
c
struct Tree {
#define ls u<<1
#define rs u<<1|1
struct Node {
int l, r, mx;
} tr[N << 2];
void pushup(int u) {
tr[u].mx = min(tr[ls].mx, tr[rs].mx);
}
void build(int u, int l, int r, vi &a) {
tr[u] = {l, r, inf};
if (l == r) {
tr[u].mx = a[l];
return;
}
int mid = (l + r) >> 1;
build(ls, l, mid, a);
build(rs, mid + 1, r, a);
pushup(u);
}
void modify(int u, int idx, int val) {
if (tr[u].l == tr[u].r) tr[u].mx = val;
else {
int mid = (tr[u].l + tr[u].r) >> 1;
if (mid >= idx) modify(ls, idx, val);
else modify(rs, idx, val);
pushup(u);
}
}
int query(int u, int l, int r) {
if (r < tr[u].l || tr[u].r < l) return inf;
if (l <= tr[u].l && tr[u].r <= r) return tr[u].mx;
return min(query(ls, l, r), query(rs, l, r));
}
int ask(int u, int l, int r, int &s) {
if (l <= tr[u].l && tr[u].r <= r) {
if (tr[u].r - l - min(s, tr[u].mx) < 0) {
s = min(s, tr[u].mx);
return -1;
}
if (tr[u].l == tr[u].r) {
return tr[u].l;
}
}
int mid = (tr[u].l + tr[u].r) >> 1;
if (l <= mid) {
int res = ask(ls, l, r, s);
if (res != -1)return res;
}
if (r > mid) {
int res = ask(rs, l, r, s);
if (res != -1)return res;
}
return -1;
}
} t;
void solve() {
int n = rd(), q = rd();
vi a(n + 1);
rep(i, 1, n) {
a[i] = rd();
}
t.build(1, 1, n, a);
rep(i, 1, q) {
int op = rd();
if (op == 1) {
int i = rd(), x = rd();
t.modify(1, i, x);
} else {
int l = rd(), r = rd();
int s = inf;
int pos = t.ask(1, l, r, s);
// cout << pos << ' ' << pos - l << ' ' << t.query(1, l, pos) << ' ';
if (t.query(1, l, pos) == pos - l) {
cout << 1 << '\n';
} else {
cout << 0 << '\n';
}
}
}
}