题目链接
对于一个人右侧需求的椅子数 r r r,另一个人左侧需求的椅子数 l l l,那么拼到一起后两个人之间的椅子数是 max ( l , r ) max(l,r) max(l,r)。对于其中的一个人来说,多摆了 max ( l , r ) − min ( l , r ) max(l,r)-min(l,r) max(l,r)−min(l,r)把椅子。
我们希望多摆的椅子最小。让 max ( l , r ) − min ( l , r ) max(l,r)-min(l,r) max(l,r)−min(l,r)最小,可以将 l , r l,r l,r分别从小到大排序。对于排序后的两个序列:
令 a n s ans ans为最终的答案,则 a n s = n + ∑ i = 1 n max ( l i , r i ) ans=n+displaystylesumlimits_{i=1}^n max(l_i, r_i) ans=n+i=1∑nmax(li,ri)。
其中 n n n为 n n n个人坐下需要的椅子,剩下的就是满足每个人两边的椅子的数量的需求。可以贪心地认为这样一定是正确的。对于排序后的序列,如果任意调换两个数的位置,都会造成差值的变大,从而导致结果的变大。
现在的问题就只剩下一个了:这样排序,不就把 l , r l,r l,r两两对应的关系打破了吗?
这样是没关系的,因为我们可以形成任意数量的环,也就是说也可以一个人自成一环。在此基础上,我们对 l , r l,r l,r排序只是对于数值的计算变得方便,实际排列的顺序并不一定是 l , r l,r l,r从小到大的顺序。我们只是将各组 l , r l,r l,r绑定了起来,但是组与组之间的顺序是任意的。 这样的话,我们可以对于前一组的 l l l,我们的下一组可以是带有那个 l l l的人的 r r r的组。这样的话总会形成一个个环。
#include502 - 区间和using namespace std; typedef long long LL; int n; int l[100005], r[100005]; void main2() { cin >> n; for (int i = 1; i <= n; ++i) { cin >> l[i] >> r[i]; } sort(l + 1, l + n + 1); sort(r + 1, r + n + 1); LL ans = n; for (int i = 1; i <= n; ++i) { ans += max(l[i], r[i]); } cout << ans; } int main() { ios::sync_with_stdio(false); cin.tie(0); cout.tie(0); LL _ = 1; // cin >> _; while (_--) main2(); return 0; }
题目链接
把区间问题放到图上来考虑:
假设已知
[
2
,
4
]
[2,4]
[2,4]和
[
4
,
6
]
[4,6]
[4,6],那么我从
2
2
2到
4
4
4连接一条双向边,从
4
4
4到
6
6
6连接一条双向边,那么
2
2
2到
6
6
6连通(
2
→
4
→
6
2rightarrow 4rightarrow 6
2→4→6),即可以认为已知
[
2
,
6
]
[2,6]
[2,6]。
假设已知
[
2
,
6
]
[2,6]
[2,6]、
[
4
,
6
]
[4,6]
[4,6]和
[
4
,
8
]
[4,8]
[4,8],那么从
2
2
2到
6
6
6、从
4
4
4到
6
6
6、从
4
4
4到
8
8
8各连一条双向边,这样
2
2
2到
8
8
8连通(
2
→
6
→
4
→
8
2rightarrow 6rightarrow 4rightarrow 8
2→6→4→8),即可以认为已知
[
2
,
8
]
[2,8]
[2,8]。
这样便有了做法:对于每一个区间 x , y x,y x,y,在 x x x到 y y y之间连接一条双向边。最后查询从 1 1 1到 n n n在图上的最短路径,如果是可达的有限数值,则可以认为已知所有数字的和。
#include503 - 选数2using namespace std; typedef long long LL; LL n, q, en = 0; LL front[200050]; struct Edge { LL v, next; }e[500050]; void addEdge(LL u, LL v) { e[++en] = {v, front[u]}; front[u] = en; } struct HeapNode { int u, d; bool operator < (const HeapNode& rhd) const { return d > rhd.d; } }; bool Dijkstra() { int d[300050]; for (int i = 1; i <= n + 1; ++i) d[i] = 1e9; d[1] = 0; priority_queue q; HeapNode tmp; tmp.u = 1; tmp.d = d[1]; q.push(tmp); while (!q.empty()) { HeapNode x = q.top(); q.pop(); int u = x.u; if (d[u] != x.d) continue; for (int i = front[u]; i; i = e[i].next) { int v = e[i].v; if (d[v] > d[u] + 1) { d[v] = d[u] + 1; tmp.u = v; tmp.d = d[v]; q.push(tmp); } } } if (d[n + 1] < 1e9) { return true; } else return false; } int main() { ios::sync_with_stdio(false); cin.tie(0); cout.tie(0); cin >> n >> q; en = 0; for (LL i = 1; i <= q; ++i) { LL x, y; cin >> x >> y; addEdge(x, y + 1); addEdge(y + 1, x); } if (Dijkstra()) { cout << "Yes"; } else cout << "No"; return 0; }
题目链接
首先选取 m m m。假设选取的 m m m个数从小到大依次为 b 1 , b 2 , ⋯ , b m b_1,b_2,cdots,b_m b1,b2,⋯,bm,设 p r e [ i ] = b 1 + b 2 + ⋯ + b i pre[i]=b_1+b_2+cdots+b_i pre[i]=b1+b2+⋯+bi。
根据题目可以列式得: p r e [ m ] ≥ m k × b m pre[m] geq frac{m}{k} times b_m pre[m]≥km×bm。
发现,如果我们 m m m取小时, p r e [ m ] pre[m] pre[m]也会相应变少。如果我们想求 m m m,如果不是二分,则需要 O ( n 2 ) O(n^2) O(n2)的时间复杂度,很不能接受。如果我们能发现其中的单调性,那就可以用二分,在 O ( n log n ) O(nlog n) O(nlogn)的时间复杂度内求出 m m m。
接下来分析单调性:若选取 a 1 , a 2 , ⋯ , a m a_1,a_2,cdots, a_m a1,a2,⋯,am时可以成立(从左到右不减),则选取 a 1 , a 2 , ⋯ , a m − 1 a_1,a_2,cdots, a_{m-1} a1,a2,⋯,am−1依旧可以成立。令 b 1 = a 1 , b 2 = a 2 , ⋯ , b m = a m b_1=a_1,b_2=a_2,cdots,b_m=a_m b1=a1,b2=a2,⋯,bm=am。
已知 p r e [ m ] ≥ m k × b m pre[m] geq frac{m}{k} times b_m pre[m]≥km×bm, p r e [ m − 1 ] ≥ m − 1 k × b m − 1 pre[m-1] geq frac{m-1}{k} times b_{m-1} pre[m−1]≥km−1×bm−1。尝试通过第一个不等式推出第二个不等式,对第一个不等式进行变形,有:
p r e [ m − 1 ] ≥ m k × b m − b m = m − 1 k × b m pre[m-1] geq frac{m}{k} times b_m-b_m=frac{m-1}{k} times b_m pre[m−1]≥km×bm−bm=km−1×bm
只需证明 m − 1 k × b m ≥ m − 1 k × b m − 1 frac{m-1}{k} times b_m geq frac{m-1}{k} times b_{m-1} km−1×bm≥km−1×bm−1即可。将不等式变形,得到:
b m ≥ m − 1 m − k × b m − 1 = ( 1 − 1 − k m − k ) × a m − 1 b_mgeq frac{m-1}{m-k} times b_{m-1}=(1-frac{1-k}{m-k})times a_{m-1} bm≥m−km−1×bm−1=(1−m−k1−k)×am−1
这是当 b 1 , ⋯ , b m b_1,cdots, b_m b1,⋯,bm满足条件时, b 1 , ⋯ , b m − 1 b_1,cdots, b_{m-1} b1,⋯,bm−1也满足条件的条件。注意到 m m m越大,条件越苛刻,所以 m m m越大,可以形成的可能性越小。换句话说,如果 m m m不满足这个条件, m m m更大的时候也不会满足这个条件了。于是简陋地分析出了单调性。
于是二分求 m m m。check这个 m m m从左到右遍历所有可能的定长区间看看有没有区间可以满足条件。有就是true。
求完 m m m后,在 m m m确定的情况下寻找不可能被选中的数。我们可以枚举所有长度为 m m m的连续区间,如果这个区间满足题目要求,那么我们尝试向左枚举左端点,将右边 m − 1 m-1 m−1个数定在原位置,最左边的数不断向左移,由于只改变这 m m m个数的和,所以单调性显然。二分查找最左侧的满足条件的点。在右边 m − 1 m-1 m−1个数都在原位置的情况下,二分求得的左端点的位置设为 l l l,设右端点在 i i i,那么可知 [ l , i ] [l,i] [l,i]内的点都是可以选择的点。我们将 [ l , i ] [l,i] [l,i]标记。枚举所有区间后,输出没有被标记的点的数量。
维护标记的方法可以用差分数组或线段树。线段树会多一个 log log log。
#include504 - 数组划分using namespace std; typedef long long LL; struct PAIR { int x, id; }a[200005]; int n, m, ai, p, q; LL ans[200005], pre[200005]; int dif[200005]; bool check(int x) { for (int i = x; i <= n; ++i) { double tmp = ((double)(pre[i] - pre[i - x]) * (double)p) / (double)((LL)x * q); if ((double)a[i].x <= tmp) { return true; } } return false; } bool check2(int i, int x) { LL sum = (pre[i] - pre[i - m + 1]) + a[x].x; double tmp = ((double)sum * (double)p) / (double)((LL)m * q); if ((double)a[i].x <= tmp) { return true; } else return false; } void main2() { cin >> n; for (int i = 1; i <= n; ++i) { cin >> a[i].x; a[i].id = i; } sort(a + 1, a + n + 1, [](const PAIR &A, const PAIR &B) { return A.x < B.x; }); cin >> p >> q; pre[0] = 0; for (int i = 1; i <= n; ++i) { pre[i] = pre[i - 1] + a[i].x; } int l = 1, r = n; m = l; while (l <= r) { int mid = (l + r) >> 1; if (check(mid)) { m = max(m, mid); l = mid + 1; } else r = mid - 1; } for (int i = 0; i <= n; ++i) { dif[i] = 0; } for (int i = n; i >= m; --i) { double tmp = ((double)(pre[i] - pre[i - m]) * (double)p) / (double)((LL)m * q); if ((double)a[i].x <= tmp) { l = 1; r = i - m + 1; int y = r; while (l <= r) { int mid = (l + r) >> 1; if (check2(i, mid)) { y = mid; r = mid - 1; } else l = mid + 1; } dif[y]++; dif[i + 1]--; } } for (int i = 1; i <= n; ++i) { dif[i] += dif[i - 1]; if (dif[i] <= 0) ans[++ai] = a[i].id; } sort(ans + 1, ans + ai + 1); cout << ai << 'n'; for (int i = 1; i <= ai; ++i) { cout << ans[i] << ' '; } } int main() { ios::sync_with_stdio(false); cin.tie(0); cout.tie(0); LL _ = 1; // cin >> _; while (_--) main2(); return 0; }
题目链接
二进制位运算求最大值,可以联想到从高位到低位判断每一个位置能否放 1 1 1。
从高到低枚举每一位,假设我们能放置 1 1 1,然后对放置 1 1 1之后的答案进行验证。验证方法如下:
在这里仅限这道题目,定义“优于”为:设 x , y x,y x,y为正整数,如果在二进制意义上,对于每一位都有 x x x在这里为 1 1 1时 y y y在这一位也为 1 1 1、 x x x在这一位里不为 1 1 1时 y y y在这一位上可能是 0 0 0,可能是 1 1 1,那么称 y y y优于 x x x。
设 d p [ i ] [ j ] dp[i][j] dp[i][j]表示对于序列的前 i i i个数,在分为 j j j段的情况下,结果是否由于我们所验证的答案 a n s ans ans。如果优于 a n s ans ans,那么其值为 1 1 1,否则为 0 0 0。
初始化 d p [ 0 ] [ 0 ] = 1 dp[0][0]=1 dp[0][0]=1。对于每一个 i i i,我们考察所有 j ( 1 ≤ j ≤ i ) j(1leq jleq i) j(1≤j≤i),看区间 [ j , i ] [j,i] [j,i]的和的值是否优于 a n s ans ans。如果优,则将前 i i i个数分为两端来看:一段是 [ 1 , j − 1 ] [1,j-1] [1,j−1],一段是 [ j , i ] [j,i] [j,i]。已知 [ j , i ] [j,i] [j,i]优于 a n s ans ans,只要在切割了 l − 1 l-1 l−1的意义上, d p [ j − 1 ] [ l − 1 ] = 1 dp[j-1][l-1]=1 dp[j−1][l−1]=1,那么 d p [ i ] [ j ] dp[i][j] dp[i][j]就可以为 1 1 1。枚举所有可能的长度 l l l,获得 d p [ i ] [ j ] dp[i][j] dp[i][j]的值。
如果最后 d p [ n ] [ k ] = 1 dp[n][k]=1 dp[n][k]=1,说明这个 a n s ans ans是ok的。
时间复杂度 O ( n 3 ) O(n^3) O(n3)。
#include505 - namonamousing namespace std; typedef long long LL; LL n, k; LL a[150], pre[150]; int dp[105][105]; bool check(LL x) { for (int i = 0; i <= n; ++i) { for (int j = 0; j <= k; ++j) { dp[i][j] = 0; } } dp[0][0] = 1; for (int i = 1; i <= n; ++i) { for (int j = 1; j <= i; ++j) { if (((pre[i] - pre[j - 1]) & x) == x) { for (int l = 1; l <= k; ++l) { dp[i][l] |= dp[j - 1][l - 1]; } } } } return dp[n][k]; } void main2() { cin >> n >> k; pre[0] = 0; for (int i = 1; i <= n; ++i) { cin >> a[i]; pre[i] = pre[i - 1] + a[i]; } LL ans = 0; for (int i = 52; i >= 0; --i) { if (check((ans | (1ll << i)))) { ans |= (1ll << i); } } cout << ans; } int main() { ios::sync_with_stdio(false); cin.tie(0); cout.tie(0); LL _ = 1; // cin >> _; while (_--) main2(); return 0; }
题目链接
注意到数据范围只有 n ≤ 40 nleq 40 n≤40,用meet in the middle来解决此题。
我们将给定的字符串分成两半,暴力枚举前一半的字符串的可能分配情况,对于每一位,给第一个人,然后往后遍历。递归返回后再给第二个人,然后往后遍历。对于左半字符串,我们分配给两个人后得到的两个字符串,如果两个字符串长度相等,那么这两个字符串也相等;如果两个字符串不等长,那么短的字符串应当是长的字符串的前缀,此时将长的字符串中不包含短字符串的后缀部分放入对应的set中(如果是第一个串长,就放到第一个set;如果是第二个串长,就放到第二个set)。如果得到了两个等长的相同的字符串,那么记录存在等长情况,不向set中放东西。
对于后半的字符串我们也采取同样的枚举方式,只不过在遍历到最后之后,短字符串应当是长字符串的一个后缀。将长字符串中去掉短字符串后剩余的前缀取出,看看set里面有没有跟这个串长的一样的(如果是第一个串长,就去第二个set里面看看有没有相同的;如果是第二个字符串长,就去第一个set里面看看有没有相同的),有则直接匹配成功。如果得到了等长的情况,那么就看前半里面有没有等长的情况,如果有,则直接匹配成功。匹配成功后,就可以直接输出并结束程序了。
这里的集合用哈希处理了字符串。
#include506 - 体育节using namespace std; typedef long long LL; const LL mod = 998244353; const LL bse = 131; string x; int len; set a, b; LL sa[44], sb[44]; int ai = 0, bi = 0; int half_same = 0, yes = 0; void dfs1(int id) { if (id >= len / 2) { if (ai < bi) { LL hsh = 0; for (int i = ai + 1; i <= bi; ++i) { hsh = (hsh * bse + sb[i]) % mod; } b.insert(hsh); } else if (ai > bi) { LL hsh = 0; for (int i = bi + 1; i <= ai; ++i) { hsh = (hsh * bse + sa[i]) % mod; } a.insert(hsh); } else half_same = 1; return; } //give to a if (ai >= bi or sb[ai + 1] == x[id] - 'a' + 1) { sa[++ai] = x[id] - 'a' + 1; dfs1(id + 1); --ai; } //give to b if (ai <= bi or sa[bi + 1] == x[id] - 'a' + 1) { sb[++bi] = x[id] - 'a' + 1; dfs1(id + 1); --bi; } } void dfs2(int id) { if (id >= len / 2) { if (ai < bi) { LL hsh = 0; for (int i = bi; i >= ai + 1; --i) { hsh = (hsh * bse + sb[i]) % mod; } if (a.count(hsh)) yes = 1; } else if (ai > bi) { LL hsh = 0; for (int i = ai; i >= bi + 1; --i) { hsh = (hsh * bse + sa[i]) % mod; } if (b.count(hsh)) yes = 1; } else { if (half_same) yes = 1; } return; } //give to a if (ai >= bi or sb[ai + 1] == x[len - 1 - id] - 'a' + 1) { sa[++ai] = x[len - 1 - id] - 'a' + 1; dfs2(id + 1); --ai; } if (yes) return; //give to b if (ai <= bi or sa[bi + 1] == x[len - 1 - id] - 'a' + 1) { sb[++bi] = x[len - 1 - id] - 'a' + 1; dfs2(id + 1); --bi; } if (yes) return; } void main2() { cin >> x; half_same = yes = 0; len = x.length(); a.clear(); b.clear(); ai = bi = 0; dfs1(0); ai = bi = 0; dfs2(0); if (yes) cout << "possiblen"; else cout << "impossiblen"; } int main() { ios::sync_with_stdio(false); cin.tie(0); cout.tie(0); LL _ = 1; cin >> _; while (_--) main2(); return 0; }
题目链接
由于题目中的序列无关顺序,所以先对序列进行排序。
错误想法:从后往前考虑, d n d_n dn时与我们所进行的排序没有关系,是一个固定值。再往前的 d n − 1 d_{n-1} dn−1如何变得最小?一定是删去序列两端的数,才能让 d n − 1 d_{n-1} dn−1变小。删去改变答案更大的那一侧,会更优。一直这样删到只剩下一个数,之后答案不会再变化。
反例:
10
11 18 17 13 1 17 13 14 13 12
对于左右相等的差值,盲目选择左侧或右侧可能会造成答案变坏。
正确做法:大体思路和上述相近,只不过设 d p [ i ] [ j ] dp[i][j] dp[i][j]表示题目的子问题的答案,即对于一个排好序的子序列 [ i , j ] [i,j] [i,j],这个问题的答案是 d p [ i ] [ j ] dp[i][j] dp[i][j]。我们发现,从小区间向大区间转移是自然的, d p [ i ] [ j ] dp[i][j] dp[i][j]可以从 d p [ i + 1 ] [ j ] dp[i+1][j] dp[i+1][j]和 d p [ i ] [ j − 1 ] dp[i][j-1] dp[i][j−1]转移,转移方程是:
dp[i][j] = a[j] - a[i] + min(dp[i + 1][j], dp[i][j - 1]);
即从小区间加上一个最小的更大值或最大的更小值,对于这个区间的扩展来说一定才是最优的。
#include507 - 测温using namespace std; typedef long long LL; int n, l, r; LL a[2005], dp[2005][2005]; void main2() { cin >> n; for (int i = 1; i <= n; ++i) { cin >> a[i]; } LL ans = 0; sort(a + 1, a + n + 1); for (int i = 2; i <= n; ++i) { for (int j = 1; j <= n; ++j) { int k = j + i - 1; if (k > n) break; dp[j][k] = a[k] - a[j] + min(dp[j + 1][k], dp[j][k - 1]); } } cout << dp[1][n]; } int main() { ios::sync_with_stdio(false); cin.tie(0); cout.tie(0); LL _ = 1; // cin >> _; while (_--) main2(); return 0; }
题目链接
参考了灯灯登登的题解。
一眼看上去是一道dp,但是怎么也不会 O ( n ) O(n) O(n)。
如果数据范围很小,那么做法就是设 d p [ i ] [ j ] dp[i][j] dp[i][j]为第 i i i天,温度为 j j j的答案,可以通过上一天温度小于等于 j j j的答案转移。
现在我们来优化这个做法。我们发现,如果
j
<
k
j 所以我们可以用一个队列来维护这个不减的区间,如果不再能被转移,就从队列中踢掉,如果从不能被转移变成能够被转移,就加入到队列当中去。为了减小维护双端队列时带来的时间开销,我们在队列中存储的是一个含值的区间,这个区间在这个区间内应当是相同的。 在读取一个新区间时: 如果当前队列所表示的区间的的左端点小于等于刚刚读入的区间的右端点,那么说明有一部分区间(或者全部)是可以更新
d
p
dp
dp值的。我们发现,每读取一次区间,这个
d
p
dp
dp值至多增加
1
1
1。所以为了减少更新数值带来的时间开销,我们用偏移量来代替更新区间内的
d
p
dp
dp值。 如果当前队列所表示的区间的右端点小于刚刚读入的区间的右端点,那么我们就要不断从右端弹出区间,直到我们最右端的区间包含了一部分会被转移到的部分。这个时候更新这个区间的右端点。 如果当前队列所表示的区间的左端点小于刚刚读入的区间的左端点,那么我们就要不断从左端弹出区间,直到我们最左端的区间包含了一部分会被转移到的部分。这个时候更新这个区间的左端点。 如果当前队列所表示的区间的左端点大于刚刚读入的区间的左端点,那么我们要从左边塞入一个区间来填补这个区间的值。注意,这个区间的
d
p
dp
dp值是
0
0
0,但是因为我们在过程中只维护了偏移量,所以赋值时要给这个区间的
d
p
dp
dp值赋值为偏移量的相反数。 如果当前队列所表示的区间的左端点大于刚刚读入的区间的右端点,说明前面维护的信息对于下面的区间来说已经没有用了,直接清除队列。 每次对一个读入的区间操作完,当前情况的最大值就是整个队列最右侧的区间的值加上偏移量,用这个值和答案比较,把答案更新为较大值。 具体的细节可以看代码。#include



