惯例记录时间,但是这次没达到就写了,原因是我突然发现自己竟然 10 天写了近 100 题,
也是很有纪念意义的

这期间打了 5、6把 CF,vp了几场 d2,d3,然后就是板刷1600~1700,还vp了一次四川省赛,哎哎哎,还是太难
然后就这样吧,本来想着今天再肝 10 题的,但是下午有课,而且昨晚 CF 上分太猛了,今天状态不佳,没怎么睡醒,迷迷糊糊的,实在肝不动了, 而且还有 3 节 SB 课要上,
综上,遂挑战失败,虽然只要再 vp 2场 d3 就够到了,但是我还是想让最后 10 题不参杂水分太多
好了,这次没什么好讲的,加训就完了
对了对了,本来说这次就把这段时间板刷记录放到这里的,也是见证了
1500 板刷
CF 2137E
// 就是给数组的每个元素计算mex,一共计算 k 次,最后数组的和是多少?
// 半天半天就是一个计数*,看值域完了
// 我还在搞整个数组变换*
void solve(){
cin >> n >> k;
vector<int> num(n+1), last(n+1), nxt(n+1), cnt(n+1,0), res;
For(i,1,n) cin >> num[i], cnt[num[i]]++;
while(k){
--k;
int Mex = 0;
while(cnt[Mex])Mex++;
nxt.assign(n+1,0);
// 查看两次怎么变换就好了,同一个数字出现多次就会全部变成Mex
For(i,0,n){
if(i<Mex && cnt[i] == 1) nxt[i] += cnt[i]; // 只出现一次,就是变成自己
else nxt[Mex] += cnt[i]; // 否则变成 Mex
}
if(last == nxt) break;
last = cnt, cnt = nxt;
}
res = cnt;
if(k&1) res = cnt;
else res = nxt;
// for(auto x : res) cerr << x << ' ';
// cerr << '\n';
ll ans = 0;
For(i,0,n) ans += 1ll*res[i]*i;
cout << ans << '\n';
}
CF 2093E
题意:
划分 k 段 mex,最大化划分最小的 mex(b)
还真就是,边扫边找,不过这次用小根堆来找到合法答案,一边扫一边找,每次划分的区间就是堆中存放的元素
就是找到第一个划分点就直接划分
// 最大化划分 k 段的最小 mex
void solve(){
cin >> n >> k;
vector<int> num(n+1);
vector<vector<int>> pos(n+1);
For(i,1,n) cin >> num[i];
auto check = [&](int mid) -> bool{
int cnt = 0, mex = 0;
priority_queue<int,vector<int>, greater<>> pq;
For(i,1,n){
if(num[i]<mid) pq.push(num[i]);
while(pq.size()) {
if(pq.top()>mex) break;
auto x = pq.top(); pq.pop();
if(x == mex) mex++;
}
if(mex == mid){
cnt++, mex = 0;
while(pq.size()) pq.pop(); // 清空当前划分段
}
}
return cnt>=k;
};
int l = 1, r = n/k+1,mid;
while(l<=r){
mid = (r+l) >> 1;
if(check(mid)) l = mid+1;
else r = mid-1;
}
cout << r << '\n';
}
CF 2077A
思路:
方程可以重排为 $a_{2n} = a_1 + (a_3 - a_2) + \dots + (a_{2n-1} - a_{2n-2}) + a_{2n+1}$
让 $a_1$ 作为最大数字,剩下的 $n$ 个大数字作为 $a_3,a_5, \dots a_{2n+1}$
这样 $a_2$ 自然大于所有数字
代码:
void solve(){
cin >> n;
vector<int> num(2*n+1);
For(i,1,n*2) cin >> num[i];
vector<int> odd, even;
sort(num.begin()+1, num.end(), greater());
For(i,1,n+1) odd.pb(num[i]);
For(i,n+2,2*n) even.pb(num[i]);
x = accumulate(all(odd),0ll) - accumulate(all(even),0ll);
even.pb(x);
For(i,1,2*n+1){
if(i&1) cout << odd.back() << ' ', odd.pop_back();
else cout << even.back() << ' ', even.pop_back();
}
cout << '\n';
}
CF 2075C
思路:
枚举划分点,只要颜色数量满足左边的数量足够且右边数量足够就能作为候选颜色,同时需要去除两边共色的情况
然后每个划分点的贡献公式就是
$#a_i*a_j, (a_i \ge t \and a_j \ge n-t) - #a_c (a_c \ge max(t,n-t))$
所以只要找每个颜色的数量覆盖区间就好了
void solve(){
cin >> n >> m;
vector<int> num(m+1), sum(n+1), cnt(n+1);
For(i,1,m) cin >> num[i], cnt[num[i]]++;
For(i,1,n) sum[i] = sum[i-1] + cnt[i];
// 颜色数量 > i 的 种类数数量
ll ans = 0;
for(int i=1;i<n;++i){
int L = sum[n] - sum[i-1], R = sum[n] - sum[n-i-1], same = sum[n] - sum[max(i,n-i)-1];
ans += 1ll*L*R - same;
}
cout << ans << '\n';
}
CF 2067C
一个位置加上9其值一定会改变,发现要加9次9能变成7,这是最多的情况,但考虑可能有进位的情况,可能需要加更多次。于是枚举加什么数,然后一直加每次判断有没有出现7,记录最小次数就行。至于加多少次,应该不到10次,不过为了保险可以加20次
// 答案最多就 9 ,每一位都在 9 次之内可以解决,考虑枚举每种 9 加多少次就够了
bool check(int x){
while(x){
if(x%10==7) return 1;
x/=10;
}
return 0;
}
void solve(){
cin >> n;
int ans = 9;
for(int i = 9;i<=1e17;i=i*10+9){
int cnt = 0,t = 10, x = n;
while(t-- && !check(x)){
x += i, cnt++;
}
if(check(x)) ans = min(ans,cnt);
}
cout << ans << '\n';
}
CF 2069C
一个1500的DP数子序列的题,拿下
void solve(){
cin >> n;vector<int> num(n+1);
For(i,1,n) cin >> num[i];
vector<vector<Z>> dp(n+1, vector<Z>(4));
For(i,1,n){ x = num[i];
for(int j=1;j<=3;++j)dp[i][j] = dp[i-1][j];
if(x == 1) dp[i][x]+=1;
else if(x == 2) dp[i][x] += dp[i-1][x] + dp[i-1][1];
else dp[i][x] += dp[i-1][2];
}
cout << dp[n][3] << '\n';
}
CF 2049C
就是看到后面那个改了就好了
void solve(){
cin >> n >> x >> y;
vector<int> ans(n+2);
For(i,1,n) if(i%2 == 0) ans[i] = 1;
if(n&1) ans[n] = 2;
if(ans[x] == ans[y]){
if(n%2 == 0) ans[x] = 2;
else {
int now = ans[y]; ans[y] = 2;
for(int i=y+1;i<=n;++i, now ^= 1) ans[i] = now;
}
}
For(i,1,n) cout << ans[i] << " \n"[i==n];
}
CF 2026C
xll还是太超模了
分析免除和必买项,
从后往前买,记录免除的个数,用 0 去免除 1, 到了 0 就减掉个数就行了
cnt 只有前面有能够用来免单的才能++,就是 cnt + 1 < i
void solve(){
cin >> n >> s, s = " " + s;
int cnt = 0, ans = 0;
for(int i = n; i>=1; --i){
if(s[i] == '1'){
if(cnt + 1 < i) ++cnt;
else ans += i;
}else{
ans += i;
if(cnt) --cnt;
}
}
cout << ans << '\n';
}
CF 2050E
DP选择子序列的最小花费,dp[i][j] 表示 a 选择前 i 个, b 选择前 j 个的最小不同个数
转移: dp[i][j] = min(dp[i-1][j] + (a[i-1] != c[i+j-1]), dp[i][j-1] + (b[j-1] != c[i+j-1]));
// dp[i][j]:a 选择 前 i 个, b 选择前 j 个字符的最小选择花费
// dp[i][j] = min(dp[i-1][j] + a[i-1] != c[i+j-1], dp[i][j-1] + b[j] != c[i+j-1])
void solve(){
string a,b,c;cin >> a >> b >> c;
n = a.size(), m = b.size();
vector<vector<int>> dp(n+1,vector<int>(m+1, inf32));
dp[0][0] = 0;
for(int i=0;i<n;++i)dp[i+1][0] = dp[i][0] + (a[i] != c[i]);
for(int i=0;i<n;++i)dp[0][i+1] = dp[0][i] + (b[i] != c[i]);
for(int i=1;i<=n;++i){
for(int j=1;j<=m;++j){
dp[i][j] = min(dp[i-1][j] + (a[i-1] != c[i+j-1]), dp[i][j-1] + (b[j-1] != c[i+j-1]));
}
}
cout << dp[n][m] << '\n';
}
CF 2007C
这是个典?
结论理解:任意整数增加 $p \cdot a + q \cdot b$,都是增加 $gcd(a,b)$ 的倍数
则,每个数字都能加到比他大同余 g 的任何数字
题意:
数组可以 +a, +b,最小化最大值和最小值之差
思路:
结论:如果对数字 +a,-a,+b,-b 任意次,我们可以得到 $a_i + k\cdot gcd(a,b)$,
这个题只能加,但是求得是差值,所以可以让其他数字相加,相对而言就是剩下数字相减了,
则,数组可以简化为 $a_i \mod gcd(a,b)$,然后贪心一下就过了这题
void solve(){
cin >> n >> A >> B;
int g = gcd(A,B);
vector<int> num(n+1);
For(i,1,n) cin >> num[i], num[i] %= g;
sort(num.begin()+1, num.end());
num.erase(unique(num.begin()+1, num.end()), num.end());
n = num.size()-1;
int ans = (num[n] - num[1]);
For(i,1,n-1) ans = min(ans,num[i] + g - num[i+1]); // 1 + 7 - 5
cout << ans << '\n';
}
CF 1990C
又是一个观察性质题
难得东西就是只出现一次得只有非单调才会出现,
但是变化一次就能变成单调了,在变换一次就可以把出现一次得杀完了
然后就是计数就完了,计数存活时间,我想复杂一次批量算一组了*
凭空增加得码量
vector<int> work(vector<int> num){
vector<int> b(n+1);
priority_queue<int> pq;
map<int,int> cnt;
For(i,1,n){
x = num[i],cnt[x]++;
if(cnt[x] >= 2) pq.push(x);
y = 0;
if(pq.size())y = pq.top();
b[i] = y;
}
return b;
}
void solve(){
cin >> n;
vector<int> num(n+1),sum(n+1);
For(i,1,n) cin >> num[i];
ll ans = accumulate(all(num),0ll);
auto b = work(num);
ans += accumulate(all(b), 0ll);
b = work(b);
map<int,int> cntb;
For(i,1,n) cntb[b[i]]++;
int mx = *max_element(all(num));
For(i,1,mx) sum[i] = sum[i-1] + cntb[i];
For(i,1,mx){
int cc = cntb[i], Cnt = 1ll*cc*(cc+1)/2;
if(cc > 1)Cnt += ((sum[mx] - sum[i]) * cc);
ans += 1ll*Cnt*i;
}
cout << ans << '\n';
}
CF 1985F
all at once
void solve(){
cin >> m >> n;
vector<int> a(n+1), c(n+1), ord(n+1);
For(i,1,n) cin >> a[i];
For(i,1,n) cin >> c[i];
priority_queue<pii,vector<pii>,greater<>> pq;
for(int i=1;i<=n;++i) pq.push({1,i});
int ans = 1;
while(m>0){
auto [time, id] = pq.top(); pq.pop();
m -= a[id];
ans = time;
pq.emplace(time + c[id], id);
}
cout << ans << '\n';
}
CF 1996D
题意:
满足 $a+b+c \le x$ 且 $ab + ac + bc \le n$ 的三元组个数
思路:
固定 $a$,$b$ 的可能取值至少为 $b \le n/a$,对于所有的 $a$,$b$ 的可能取值个数就是 $n/1 + n/2 + \dots n/n$,是调和级数级别的,个数复杂度是 $\log{n}$
枚举所有 $a$,枚举所有 $b$,然后答案就是 $c$ 的可能个数
$c = min((n-ab)/(a+b), x-(a+b))$
代码:
// 固定 a,枚举 b 的可能,然后枚举所有的 a
// 答案就是可能的 a 和 b 中 c 的总和
void Solve(){
cin >> n >> x;
ll ans = 0;
For(a,1,min(n,x)){
for(int b = 1; a*b <= n && a + b <= x; ++b){
ans += min((n - a*b)/(a+b), x - (a+b));
}
}
cout << ans << '\n';
}
CF 1950E
题意:
字符串的最小周期,满足最多只有一个数字不相等
思路:
周期 k 肯定可以整除 n,则枚举所有约数即可,$10^5$ 的最大约数是 $128$,
选择前缀 l 个作为周期,或者后缀 l 个作为周期检查两边就行了
时间复杂度 $O(128 n)$
void Solve(){
cin >> n >> s;
auto f = [&](string s, int l)->bool{
int cnt=0;
for(int i=0;i<l;++i){
for(int j=i+l;j<n;j+=l) if(s[j]!=s[i])cnt++;
}
return cnt <= 1;
};
for(int l = 1; l <= n;++l){
if(n%l==0){
if(f(s,l)) return cout << l << '\n',void();
string t(s.rbegin(),s.rend());
if(f(t,l)) return cout << l << '\n',void();
}
}
}
CF 1926E
题意:
n 个数字,第一次放出奇数数字,第二次放出奇数的2倍的数字,第三次放出奇数的3倍的数字...
求第 k 次放出多少数字?
思路:
按照 $1 \times odd$, $2\times odd$,$3\times odd$,... 这样放置下去,只有 $2$ 的幂次才能真正放下,其他的牌在之前就放完了
而且每次放置的数量是 $\lceil \frac{n}{2} \rceil$,剩下 $\lfloor \frac{n}{2} \rfloor$ 个,对于每个 2 的幂次都这样取数,分块
最后就拿到了第 k 个所在的块,找到位置,
答案就是 $2^t \times (2rk -1)$
// 又读错了
// 迄今为止读错的题就没做出来过
void Solve(){
cin >> n >> k;
vector<int> vec;
while(n){
vec.pb((n+1)/2); // 多少个分为一组, 每 (n+1)/2 分为一组,剩下n/2继续分
n>>=1;
}
int tot = 0, pw2 = 1;
for(auto cnt : vec){
if(tot < k && k <= tot + cnt)return cout << pw2 * (2*(k-tot)-1) << '\n', void();
pw2 <<= 1, tot += cnt;
}
}
CF 1924A
发现自己字符串匹配子序列不会*
枚举TLE了,复杂度是 $k^n$ 级别,分析错了
bool ok;
// 子序列匹配
bool match(string s,string t){
int j = 0;
for (char ch : s) {
if (j < (int)t.size() && ch == t[j]) ++j;
}
return j == (int)t.size();
}
void dfs(int pos, vector<int> now){
if(!ok) return;
if(pos == n){
string t = "";
for(auto x : now) t += char('a' + x);
if(!match(s,t)) ok = 0, ans = t;
return;
}
for(int i=0;i<k;++i){
now.pb(i);
dfs(pos+1,now);
now.pop_back();
}
}
void Solve(){
cin >> n >> k >> m >> s;
ok = 1;
dfs(0,{});
if(ok) cout << "YES\n";
else cout << "NO\n" << ans << '\n';
}
CF 1891C
就是两种攻击方式,第一种,只减少一个,同时计数器 +1, 第二种减少 x 个,计数器清空
最少攻击次数,使得所有群组都清空
用deque模拟这个刀掉的过程就好了
代码写得好丑
void solve(){
cin >> n;
vector<int> num(n+1);
For(i,1,n) cin >> num[i];
sort(all(num));
deque<int> dq;
for(int i=1;i<=n;++i) dq.push_back(num[i]);
int now = 0, ans = 0;
while(dq.size()>1){
while(dq.size()>1 && now < dq.back()){
auto x = dq.front(); dq.pop_front();
now += x, ans += x;
}
// cerr << now << '\n';
if(now >= dq.back()){
now -= dq.back(); dq.pop_back();ans++;
if(now){
dq.push_front(now);
ans -= now, now = 0;
}
}else break;
}
ll sum = accumulate(all(dq),0ll);
ans -= now, sum += now;
// cerr << ans << '\n';
if(sum) {
ans++;
if(sum>1)ans += sum/2 + (sum&1);
}
cout << ans << '\n';
}
CF 1873G
ez结论题
void solve(){
cin >> s, n = s.size(), s = " " + s;
vector<pair<int,int>> todo;
ll sum = 0, mn = n, cA = 0, cB = 0, ok = 0;
for(int i=1;i<=n;++i){
int j = i;
while(j <= n && s[j] == s[i]) ++j;
int len = j-i, id = s[i] - 'A' + 1;
todo.pb({id, len});
if(id == 1) cA++, sum += len, mn = min(mn, len);
else {
cB++;
if(len > 1) ok = 1;
}
i = j-1;
}
if(todo.front().first == 1 && cA > cB && !ok){
sum -= mn;
}
cout << sum << '\n';
}
CF 1883D
我到底写得什么题啊,这么简单
拿两个multiset模拟完了,每次就找有没有比最小的r大的l,完事
void solve(){
cin >> q;
multiset<int> L,R;
while(q--){
char op; cin >> op >> l >> r;
if(op == '+') L.insert(l), R.insert(r);
else {
auto it1 = L.find(l);
L.erase(it1);
auto it2 = R.find(r);
R.erase(it2);
}
if(R.empty()) puts("NO");
else{
int mnr = *R.begin();
auto it = L.upper_bound(mnr); // 找第一个比 mnr 大的位置
if(it != L.end()) puts("YES");
else puts("NO");
}
}
}
CF 1949B
终于硬控了
题意:
最大化差值绝对值的最小值
思路:
就是枚举分界点,由绝对值定义划分为两种集合
在两种集合中大的小的配对,然后答案就是各种划分的最大值
枚举划分方式即可
最小的 t 个a 和最大的 n-t 个 b 配对
最小的 t 个b 和最大的 n-t 个 a 配对
最大化最小的差值,就是让前k个a与前k个b配对算差值
void solve(){
cin >> n;
vector<int> a(n+1), b=a;
For(i,1,n) cin >> a[i];For(i,1,n) cin >> b[i];
sort(a.begin()+1,a.end()); sort(b.begin()+1,b.end());
int ans = 0;
for(int t = 1; t <= n; ++t){
int A = 0, B = 0, mn = 1e9;
for(int i=1;i<=t;++i){
mn = min(mn, abs(b[n - t + i] - a[i]));
}
int mn2 = 1e9;
for(int i = t+1; i<=n; ++i){
mn2 = min(mn2, abs(a[i] - b[i-t]));
}
ans = max(ans,min(mn,mn2));
}
cout << ans << '\n';
}
CF 1881E
dp[i]:前 i 个元素分块的最小删除数量
直接删除位置 i,或者作为某段的结尾
cost = min(dp[i-1] + 1, min(dp[j-1] - j - a[j]))
一个DP,推式子,求前缀min,树状数组优化DP
$dp[i]$:前 i 个位置合法的最小删除数量
考虑每个位置i,两种来源,直+接删除位置i,花费 $dp[i] = dp[i-1] + 1$
作为前面某个块的终点,包括删除字符
设前面某个块的起点为 j,则花费为 $i-j+1 - (j + a[j])$,总长度 - 删除数量
这种情况总花费 = $dp[j-1] + cost$,然后就可以拿去更新 $dp[i]$
对于第二种情况,可以从所有的可加入的前缀 $min$ 转移过来
总之: $dp[i] = min(dp[i-1] + 1, min(dp[j-1] + i - j - a[j]))$;
这里用树状数组优化就行了
// 删除最少元素使得序列分块
// 好像是DP的题
void solve(){
cin >> n;vector<int> num(n+1);
For(i,1,n) cin >> num[i];
vector<vector<int>> bucket(n+1);
For(j,1,n){
x = j + num[j];
if(x<=n)bucket[x].pb(j);
}
vector<int> dp(n+1,INT_MAX); dp[0] = 0;
ll cur = INT_MAX;
for(int i=1;i<=n;++i){
for(auto p : bucket[i]){
cur = min(cur, dp[p-1] - p - num[p]); // 作为终点,枚举块的起点
}
dp[i] = min(dp[i], dp[i-1] + 1);
if(cur < INT_MAX) dp[i] = min(dp[i], i + cur);
}
cout << dp[n] << '\n';
}
struct BIT {
int n;
vector<int> f;
BIT(int _n):n(_n),f(n+1,INT_MAX){}
void add(int i,int v){for(;i<=n;i+=i&-i)f[i]=min(f[i],v);}
int sum(int i){int s=0;for(;i>0;i-=i&-i)s+=f[i];return s;}
int query(int i){int res=INT_MAX;for(;i>0;i-=i&-i)res=min(res,f[i]);return res;}
};
void solve(){
cin >> n; vector<int> num(n+1);
For(i,1,n) cin >> num[i];
BIT bit(n+1);
vector<int> dp(n+1, INT_MAX);
dp[0] = 0;
int cur = INT_MAX;
for(int i=1;i<=n;++i){
x = i + num[i];
if(x <= n) bit.add(x,dp[i-1] - i - num[i]);
dp[i] = min(dp[i], dp[i-1] + 1);
dp[i] = min(dp[i], bit.query(i) + i);
}
cout << dp[n] << '\n';
}
CF 1915F
1500 这是上强度是吗?
树状数组的板子应用都来了
对于每个位置,
查找 $b_j \ge b_i $ 且 $a_j < a_i $ 的位置数量就行了
离散化树状数组?不至于
数据结构小手子,就这点法子了
// 查询 值 >= x 的数字的个数
// 对 b 维护一个树状数组
// 数字个数 = 总个数 - cnt(<x) (比x小的个数)
struct BIT {
int n;
vector<int> f;
BIT(int _n):n(_n),f(n+1,0){}
void add(int i,int v){for(;i<=n;i+=i&-i)f[i]+=v;}
int sum(int i){int s=0;for(;i>0;i-=i&-i)s+=f[i];return s;}
int query(int l,int r){return l>r?0:sum(r)-sum(l-1);}
};
void solve(){
cin >> n;
vector<int> a(n+1), b(n+1), todo(n+1), lsh,ord(n+1);
For(i,1,n) cin >> a[i] >> b[i], lsh.pb(b[i]);
iota(all(ord),0);
sort(ord.begin()+1,ord.end(), [&](int i, int j){
return a[i] < a[j];
});
// For(i,1,n) cerr << ord[i] << " \n"[i==n];
sort(all(lsh));
lsh.erase(unique(all(lsh)),lsh.end());
auto get = [&](int x) -> int{
auto id = lower_bound(all(lsh), x) - lsh.begin()+1;
return id;
};
BIT bit(n+1);
ll ans = 0;
For(p,1,n){
int i = ord[p], x = b[i], id = get(x);
bit.add(id, 1);
ans += p - bit.query(1,id); //总个数 - 小于等于 x 的个数
}
cout << ans << '\n';
}
CF 1851E
void init(){
For(i,1,n) G[i].clear();
}
void solve(){
cin >> n >> k; init();
vector<int> ans(n+1), deg(n+1), uni(n+1);
For(i,1,n) cin >> ans[i];
For(i,1,k) {
int id; cin >> id, ans[id] = 0;
}
For(i,1,n){
cin >> m;
For(j,1,m) cin >> x, G[x].pb(i), deg[i]++;
}
queue<int> q;
For(i,1,n) if(deg[i]==0) q.push(i), uni[i] = ans[i];
while(q.size()){
auto u = q.front(); q.pop();
for(auto v : G[u]){
uni[v] += uni[u];
if(--deg[v] == 0){
uni[v] = min(ans[v], uni[v]);
q.push(v);
}
}
}
// For(i,1,n) cerr << uni[i] << " \n"[i==n];
For(i,1,n) ans[i] = min(ans[i], uni[i]);
For(i,1,n) cout << ans[i] << " \n"[i==n];
}
CF 1850G
题意:
平面上所有斜率为 1, -1,0,的直线上共线点选两个的方案数
思路:
斜率相等的直线上点的个数
斜率为 k 的点在直线的表示形式:
常数为 b ,斜率为 1 的直线上的点可以用点表示出来
cnt[y-x]:y-x 为常数 b 的所有点都落在斜率为 1 的直线上
cnt[y+x]:y+x 为常数 b 的所有点都落在斜率为 -1 的直线上
斜率相等的直线的表示方式:
cnt[b]:常数为 b,斜率为 k 的直线上点的个数
然后对所有的斜率枚举所有常数就好了,常数就能代表一条直线了
//x, y, x + y, x - y
void solve(){
cin >> n;
map<int,int> cntx, cnty, cntyx, cntxy;
For(i,1,n){
cin >> x >> y;
cntx[x]++, cnty[y]++, cntyx[y-x]++, cntxy[x+y]++;
}
ll ans = 0;
auto cal=[&](map<int,int> mp)->ll{
ll res = 0;
for(auto [_, cnt] : mp){
res += 1ll*cnt*(cnt-1);
}
return res;
};
ans += cal(cntx) + cal(cnty) + cal(cntyx) + cal(cntxy);
cout << ans << '\n';
}
CF 1792C
分别位于首尾的 1 和 n 一定是在最后一次操作中归位的。
类似地,2 和 n−1 一定是在倒数第二次操作中归位的。以此类推。
假设共操作 k 次,则 1∼k 和 n−k+1∼n 都被归位,而中间的 k+1∼n−k 按原顺序排列。
设元素 i 的初始位置为 posi 。
那么 k 次操作能将 p 排序,当且仅当 posk+1<posk+2<⋯<posn−k 。
首先 k0=⌊2n⌋ 一定可行。
如果 k 可行,并且 posk<posk+1, posn−k<posn−k+1 ,那么 k−1 也可行。
从 k0 开始枚举,找到最小的 k 即可。
时间复杂度 O(∑n) 。
// pos_k < pos_(k+1), pos_(n-k) < pos_(n-k+1)
void solve(){
cin >> n;
vector<int> num(n+1), idx(n+1);
For(i,1,n) cin >> x, num[i] = x, idx[x] = i;
k = n/2;
while (k&&idx[k]<idx[k+1]&&idx[n-k+1]>idx[n-k]) --k;
cout << k << '\n';
}
CF 1994C
题意:
思路:
dp 划分
dp[i]:i 位置作为划分点能拿到的区间个数,$i \in (l,r)$
对于每个位置,从后往前找到第一个 g > x 的位置 j
j 开始变成 0 了,之后问题变成从 j 开始的非零区间段的个数,
这样,这个问题可以拆解,也就是 DP,可以类似看作就是划分区间的方案数问题
注意:
-
从后往前DP找子问题,
-
从 n-1 到 0,upperbound 找到的 j > i, 所以 j 的可能取值范围就是 [1,n]
void solve(){
cin >> n >> m;
vector<int> num(n+1), dp(n+2);
for(int i=1;i<=n;++i) cin >> num[i];
partial_sum(num.begin()+1,num.end(), num.begin() + 1);
ll ans = 0;
for(int i=n-1;i>=0;--i){
int j = upper_bound(all(num), num[i] + m) - num.begin();
// cerr << j << '\n';
dp[i] = dp[j] + j-i-1;
ans += dp[i];
}
cout << ans << '\n';
}
// 另一个版本,本质一样,定义细节不同
void solve(){
cin >> n >> m;
vector<int> num(n+1), dp(n+3);
for(int i=1;i<=n;++i) cin >> num[i];
partial_sum(num.begin()+1,num.end(), num.begin() + 1);
ll ans = 0;dp[n+2] = 0;
for(int i=n;i>=1;--i){
int j = upper_bound(all(num), num[i-1] + m) - num.begin();
// cerr << j << '\n';
dp[i] = dp[j+1] + j-i;
ans += dp[i];
}
cout << ans << '\n';
}
CF 1980E
我还以为T掉了,直接A了可还行
void solve(){
cin >> n >> m;
int tot = n*m;
vector<vector<int>> col(m+1), row(n+1);
vector<int> C(tot+1), R(tot + 1);
For(i,1,n)For(j,1,m)cin >> x, col[j].pb(x), row[i].pb(x);
For(i,1,n) For(j,1,m) cin >> x, C[x] = j, R[x] = i;
bool ok = 1;
For(i,1,n){
int idx = R[row[i].front()];
for(auto x : row[i]) if(R[x] != idx) {
ok = 0;
break;
}
}
if(ok){
For(j,1,m){
int idx = C[col[j].front()];
for(auto x : col[j]) if(C[x] != idx) {
ok = 0;
break;
}
}
}
puts(ok ? "YES" : "NO");
}
CF 2070D
经典DP,逐层转移,没什么好讲的,有避免TLE的办法就不要冒险,尤其是OI赛制,谨慎
using Z = ModInt<i64, i64(998244353)>;
void solve(){
cin >> n;
vector<int> dep(n+1), fa(n+1);
dep[1] = 1;
For(i,2,n) cin >> p,fa[i]=p, dep[i] = dep[p] + 1;
int H = *max_element(all(dep));
vector<vector<int>> nodes(H+1);
For(i,1,n) nodes[dep[i]].pb(i);
queue<int> q;
for(int i=1;i<=H;++i) {
for(auto u : nodes[i]) q.push(u);
}
vector<Z> dp(n+1), sum(H+1);
dp[1] = 1, sum[1] = 1;
while(q.size()){
auto u = q.front(); q.pop();
int d = dep[u];
if(d == 2) dp[u] = sum[d-1], sum[d] += dp[u];
else if(d>2){
dp[u] = sum[d-1] - dp[fa[u]], sum[d] += dp[u];
}
}
Z ans = 0;
For(i,1,n) ans += dp[i];
cout << ans << '\n';
}
CF 2034D
这个题模拟,用个set
思路:
最后的0在最前的1之前就交换,最后的2在最前的1就换,然后根据大小关系chk一下是不是升序,完了
// 就是模拟,用 最前的 1 交换最后的 0, 最前的 2 交换最后的 1
void solve(){
cin >> n;
vector<set<int>> pos(3);
For(i,1,n) cin >> x, pos[x].insert(i);
auto ck = [&]()->bool{
vector<int> id;
for(int i=0;i<=2;++i) if(pos[i].size()) id.pb(i);
if(id.size()==1) return 1;
else if(id.size() == 2){
return *pos[id.front()].rbegin() < *pos[id.back()].begin();
}else{
return (*pos[0].rbegin() < *pos[1].begin() && *pos[1].rbegin() < *pos[2].begin());
}
};
vector<pii> ans;
auto work = [&](int i){
if(pos[i].size() && pos[i+1].size()){
x = *pos[i].rbegin(), y = *pos[i+1].begin();
if(x > y){
ans.eb(x,y);
pos[i].erase(x), pos[i+1].erase(y);
pos[i].insert(y), pos[i+1].insert(x);
}
}
};
// cerr << "run\n";
while(!ck()){
work(0);
if(ck())break;
work(1);
}
cout << ans.size() << '\n';
for(auto [a,b] : ans) cout << a << ' ' << b << '\n';
}
CF 2065E
构造:
就还是消耗的思路完了,就是特判无解的时候看了题解(
然后改两行A了,哎哎哎
void solve(){
cin >> n >> m >> k;
if(max(n,m)<k) return cout<<-1<<'\n', void();
if(n >= m){
n -= k;
if(n>m)return cout<<-1<<'\n', void();
for(int i=1;i<=k;++i) cout<<'0';
while(n&&m)cout<<"10", n--,m--;
while(m--)cout<<'1';
cout<<'\n';
}else{
m -= k;
if(m>n)return cout<<-1<<'\n', void();
for(int i=1;i<=k;++i)cout<<'1';
while(n&&m)cout<<"01", n--,m--;
while(n--)cout<<'0';
cout<<'\n';
}
}
CF 2074D
(1800) 1500腻了,找点难一点的
void Solve(){
cin >> n >> k;
vector<int> num(n+1);
For(i,1,n) cin >> num[i];
auto b = num;
sort(all(b));
int w = b[k];
vector<int> c,d;
int cnt = 0;
for(int i=1;i<=n;++i){
if(num[i] < w){
c.pb(num[i]);
d.pb(cnt);
cnt = 0;
}else if(num[i] == w) cnt ++;
}
d.pb(cnt);
for (int i = 0; i < d.size(); i++) {
d[i] = min(d[i], d[d.size() - i - 1]); // 每个空里等于 w 的数量也要构成回文串,左右对称
}
vector<int> t(c.rbegin(),c.rend());
if(c != t) return puts("NO"), void();
if(accumulate(all(d),0) + c.size() < k-1) return puts("NO"), void();
puts("YES");
}
CF 2086D
题意:
26个字母,一些字符放在奇数位,一些字母放在偶数位,总共有多少种方案
思路:
背包方案数 DP
容易发现这是个背包方案数问题,
设偶数位放置字母个数为 $\frac{n}{2}$,设 $f[k][j]$ 表示放到了第 $k$ 个字母时,偶数位放了 $j$ 个的方案数,
设第 $k$ 位字母数量为 $c_k$,
若 $c_k \not = 0$,考虑这些字母放置偶数位还是奇数位,有两种选择
则有 $f[k][j] = f[k-1][j-c_k] \cdot \binom{n/2 - (j-c_k)}{c_k} + f[k-1][j] \cdot \binom{n-n/2 - j}{c_k}$
答案的方案数就是放到最后一个字母时的所有的偶数方案数之和
void Solve(){
n = 0;
vector<int> cnt(26);
for(int i=0;i<26;++i) cin >> cnt[i], n += cnt[i];
vector<Z> f(n/2+1);
f[0] = 1;
int s = 0;
for(int i=0;i<26;++i)if(cnt[i]){
vector<Z> g(n/2+1);
// 避免使用同一层转移后的值,逆序更新
for(int j=n/2;j>=0;--j){
Z t = f[j];
f[j] = 0;
// 偶数放了 j 个的方案,每次都要重新算
if(j >= cnt[i])f[j] += f[j-cnt[i]] * binom(n/2-(j-cnt[i]), cnt[i]);
if(s >= j) f[j] += t * binom(n-n/2-(s-j), cnt[i]);
}
s += cnt[i];
// f.swap(g);
}
cout << f[n/2] << '\n';
}
CF 2048D
思路:
比我弱的人不可能贡献,剩下三种题目,你会我会的,你会我不会,你不会我不会的,只有第二类题目有贡献
每个题目的贡献 $b_i$ 为会的人的数量+1
一个调和级数的复杂度,完全OK的
$O(n\log{n})$
void Solve(){
cin >> n >> m;
vector<int> a(n+1), b(m+1);
For(i,1,n)cin>>a[i];
For(i,1,m)cin>>b[i];
if(n != 1) sort(a.begin()+2,a.begin()+n+1,greater<>()), n = lower_bound(a.begin()+2,a.begin()+n+1,a[1], greater<>()) - a.begin() - 1;
sort(a.begin()+2,a.begin()+n+1,greater<>());
For(i,1,m) b[i] = b[i] <= a[1] ? 0 : upper_bound(a.begin()+2,a.begin()+n+1,b[i], greater<>()) - a.begin()-2;
sort(all(b));
// For(i,1,m)cerr<<b[i]<<" \n"[i==m];
for(int k=1;k<=m || (cout << '\n', 0);++k){
int ans = m/k;
for(int i=k;i<=m || (cout << ans << ' ', 0);i+=k){
ans += b[i];
}
}
}
CF 2028C
题意:
区间划分 m+1 段,其中 m 段的区间和必须 >= val,只有一段的值可以任意
求划分后任意段的最大值,如果无法划分,输出 -1
思路:
区间划分
预处理 1~i, i~n 的最大划分数量,
枚举答案区间的左端点,
查找符合条件的划分的右端点,就是区间端点了
然后就用区间和更新答案了
// 枚举答案区间,找到最大的右端点
// 预处理 1~i,i~n 的划分最多划分区间数量
void Solve(){
cin >> n >> m >> k;
vector<int> num(n+1), sum(n+1);
For(i,1,n) cin >> num[i], sum[i] = sum[i-1] + num[i];
int now = 0;
vector<int> pre(n+1),suf(n+2);
for(int i=1;i<=n;++i) {
now += num[i];
pre[i] = pre[i-1];
if(now>=k) pre[i]++, now = 0;
}
now = 0;
for(int i=n;i>=1;--i){
now += num[i];
suf[i] = suf[i+1];
if(now>=k) suf[i]++,now=0;
}
if(pre[n]<m) return cout<<"-1\n",void();
int ans = 0;
for(int i=1;i<=n;++i){ // 枚举区间左端点,查找最大的右端点
int L = pre[i-1], R = m - L;
// 3 3 2 2 1
int p = upper_bound(suf.begin()+i,suf.end(), R, greater<>()) - suf.begin()-1;
// 找到第一个大于的位置,往下就是要求的数量了,找到的是最右边端点,也可以最大化区间和
if(suf[p] < R) continue;
ans = max(ans, sum[p-1] - sum[i-1]);
}
cout << ans << '\n';
}
CF 2036E
模拟
一个简单的找最值我给他把复杂度从 O(1) 变成了 O(n),疯狂TLE,还增加了好多代码量,耗时耗力不讨好
这次我是真蠢,这本来一个SB题都不敢相信有这么简单
void Solve(){
cin >> n >> m >> k;
vector<vector<int>> a(n+1, vector<int>(m+1)), b = a;
For(i,1,n) For(j,1,m) b[i][j] = a[i][j] = (cin >> a[i][j], a[i][j]), b[i][j] |= b[i-1][j];
vector<vector<int>> c(m+1, vector<int>(n+1));
For(j,1,m) For(i,1,n) c[j][i] = b[i][j];
while(k--){
cin >> q;
int ok = 1, l = 1, r = n+1;
while(q--){
int x, y; char op;
cin >> x >> op >> y;
const auto& v = c[x];
if(op == '>'){
int idx = upper_bound(v.begin()+1, v.end(), y) - v.begin();
l = max(l, idx);
}else{
int idx = lower_bound(v.begin()+1, v.end(), y) - v.begin();
r = min(r, idx);
}
}
if(l >= r) cout << "-1\n";
else cout << l << '\n';
}
}
CF 2043C
题意:
所有子数组的区间和的所有可能值,最多只有1个数任意,其他元素都是 1 或者 -1
思路:
不好评价了,答案肯定是一段连续的,然后找到四个上下界就行了
其实就是最大子段和和最小子段和,然后带上 x 的前后缀查找,结束了
但是代码写得很丑,不好评价了
void Solve(){
cin >> n;
vector<int> a(n+1), vec;
For(i,1,n) cin >> a[i];
int now = 0, mn = 0, mx = 0, pos = -1;
for(int i=1;i<=n;++i){
int j = i;
while(j <= n && a[j] == a[i]) ++ j;
int len = j - i;
now = a[i]*len;
vec.pb(now);
if(a[i] != 1 && a[i] != -1){
pos = vec.size();
}else mn = min(mn, now), mx = max(mx, now);
i = j - 1;
}
int x;
if(pos>0) {
--pos,x = vec[pos];
}
m = vec.size();
now = 0;
for(int i=0;i<m;++i){
if(pos>=0 && i == pos) {
now = 0;
continue;
}
now = max(vec[i], now + vec[i]);
mx = max(mx, now);
}
now = 0;
for(int i=0;i<m;++i){
if(pos>=0 && i == pos) {
now = 0;
continue;
}
now = min(vec[i], now + vec[i]);
mn = min(mn, now);
}
// cerr << mx << '\n';
int l,r;
if(pos>=0){
now = 0, l = x, r = x;
int preMx = 0, preMn = 0, now = 0, sufMx = 0, sufMn = 0;
for(int i=pos-1;i>=0;--i){
now += vec[i];
preMx = max(preMx, now);
preMn = min(preMn, now);
}
now = 0;
for(int i=pos+1;i<vec.size();++i){
now += vec[i];
sufMx = max(sufMx, now);
sufMn = min(sufMn, now);
}
l += preMn + sufMn;
r += preMx + sufMx;
}
// cerr << mn << ' ' << mx << '\n';
// cerr << l << ' ' << r << '\n';
set<int> ans;
ans.insert(0);
for(int i=mn;i<=mx;++i) ans.insert(i);
if(pos>=0)for(int i=l;i<=r;++i) ans.insert(i);
cout << ans.size() << '\n';
for(auto x : ans) cout << x << ' ';
cout << '\n';
}
CF 2004D
模拟
这个题码量怎么这么大aaaaa
思路就一句话:能直接走就直接走,不能直接走就考虑中转点,最小化,就三种可能,2k-(u+v), (u+v)-2k
注意:一开始预处理会是O(n^2)的,所以在查询的时候去找候选值才是可接受的,
然后就没了
仔细一看,其实也还行
string color = "BGRY";
int cho[6]{3,5,6,9,10,12};
int id(int a,int b){
return (1<<a) + (1<<b);
}
void Solve(){
cin >> n >> q;
vector<vector<int>> vec((1<<4));
vector<int> num(n+1);
For(i,1,n){
string s; cin >> s;
num[i] = id(color.find(s[0]), color.find(s[1]));
vec[num[i]].pb(i);
}
while(q--){
int u,v,A,B; cin >> u >> v;
if(u == v) {cout << 0 << '\n';continue;}
if(u > v) swap(u,v);
A = num[u], B = num[v];
if((A & B) != 0){
cout << v - u << '\n';
continue;
}
int bitA[2]{},bitB[2]{}, pa=0,pb=0;
for(int i=0;i<4;++i)if(A>>i & 1) bitA[pa++] = i;
for(int i=0;i<4;++i)if(B>>i & 1) bitB[pb++] = i;
vector<int> opt;
for(int i=0;i<2;++i){
for(int j=0;j<2;++j){
opt.pb((1<<bitA[i]) + (1<<bitB[j]));
}
}
ll ans = 1e18;
bool ok = 0;
for(auto msk : opt){
auto &V = vec[msk];
auto it = lower_bound(all(V), u);
if(it != V.end() && *it <= v){
ans = min(ans,v-u);
ok = 1;
break;
}
}
if(ok) {
cout << ans << '\n';
continue;
}
for(auto msk : opt){
auto &V = vec[msk];
auto it = lower_bound(all(V), u);
if(it != V.begin()){
k = *(prev(it));
ans = min(ans,u+v-2*k);
}
if(it != V.end()){
k = *it;
ans = min(ans, 2*k-u-v);
}
}
if(ans >= 1e18) ans = -1;
cout << ans << '\n';
}
}
CF 1978D
思路:
就是发现是最大的,就不动
不是的话,
如果删除前缀 + a[i] >= mx,那就直接 i-1
否则 f[i]++,还不够,那就必须再删掉最大的了
虽然还不够优雅,但是这次我也尽量把代码写好看了点
void Solve(){
cin >> n >> m;
vector<int> num(n+1);
For(i,1,n) cin >> num[i];
int pos = -1, mx = *max_element(all(num));
for(int i=1;i<=n;++i) if(mx == num[i]) {pos = i;break;}
if(num[1] + m >= mx) pos = 1;
vector<int> f(n+1);
f[1] = (pos == 1 ? 0 : 1);
int sum = m + num[1];
for(int i=2;i<=n;++i){
sum += num[i];
if(i == pos) f[i] = 0;
else{
f[i] = i-1;
if(sum < mx) f[i]++;
}
}
For(i,1,n) cout << f[i] << " \n"[i==n];
}
CF 1976C
小坑,但是没有大碍
维护后面第一个没有选到想要的位置,nxt[i],然后让这个位置给他,他的位置给最后一个人
如果没有这样的位置,直接将该位置给到最后一人
void Solve(){
cin >> n >> m;
vector<int> bel(n+m+2), opt(n+m+2), nxt(n+m+2,-1); // 1 2
vector<vector<int>> num(n+m+2, vector<int>(3));
For(j,1,2) For(i,1,n+m+1) cin >> num[i][j];
int curn = n, curm = m, tot = 0;
for(int i=1;i<=n+m;++i){
int a = num[i][1], b = num[i][2];
if(a > b){
opt[i] = 1;
if(curn) bel[i] = 1, tot += a, -- curn;
else bel[i] = 2, tot += b, -- curm;
}else{
opt[i] = 2;
if(curm) bel[i] = 2, tot += b, --curm;
else bel[i] = 1, tot += a, --curn;
}
}
int last = -1;
for(int i = n+m; i>=1; --i){
nxt[i] = last;
if(bel[i] != opt[i]) last = i;
}
vector<int> ans(n+m+2, tot);
for(int i=1;i<=n+m;++i){
ans[i] += -num[i][bel[i]];
if(nxt[i] != -1){
int pos = nxt[i];
ans[i] += -num[pos][bel[pos]] + num[pos][bel[i]] + num.back()[bel[pos]];
}else{
ans[i] += num.back()[bel[i]];
}
}
For(i,1,n+m+1) cout << ans[i] << " \n"[i == n+m+1];
}
CF 1946C
题意:
将 n 个点的树分成 m+1 个块,每个块的最大大小是多少?
思路:
比较有趣的一题,没做出来,看题解的
显然二分
如果能成块,就直接拿上该父亲,返回 0,表示没有剩余可用
否则不能成块,直接把所有点给返回了,在父亲成块
这样,就从下往上彻底堵住了所有可能性了,很好的想法,
本质就是贪心,既然成块,就从下往上找点,如果能成块直接成了,不能就给父亲看看能不能
我最初就想着自上往下传递,看每个儿子能不能满足需求,但这样的思路太容易挂了,
细节没处理成功,哎哎哎,不能这么容易放弃
// n >= (m + 1) * x
bool ck(int mid){
int cnt = 0;
auto dfs = [&](auto &&self, int u,int fa) -> int{
int sum = 1, flg = 1;
for(auto v : G[u]){
if(v == fa) continue;
sum += self(self,v,u);;
}
if(sum >= mid) {
++cnt;
return 0;
}else return sum;
};
dfs(dfs,1,0);
return cnt>=m+1;
}
void Solve(){
cin >> n >> m; For(i,1,n) G[i].clear();
For(i,1,n-1) cin >> u >> v, G[u].pb(v), G[v].pb(u);
int l = 1, r = n/(m+1),mid,ans=1;
while(l<=r){
mid = r+l >> 1;
if(ck(mid)) l = mid+1, ans = mid;
else r = mid-1;
}
cout << ans << '\n';
}
CF 2094F
题意:
填格子,n*m % k =0, 要求相邻数字不同,且 1~k 都填满
思路:
直接 1~k 填就行了,但是,m 是 k 的倍数就将奇数行移位一次即可
这个题aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
怎么这么折磨
好简单的就一句话,怎么想那么就都没想出来
void Solve(){
cin >> n >> m >> k;
vector<vector<int>> ans(n+5,vector<int>(m+5));
For(i,1,n)For(j,1,m){
ans[i][j] = ((i-1)*m + j + (i%2 && m%k==0)-1) % k + 1;
}
For(i,1,n) For(j,1,m) cout << ans[i][j] << " \n"[j == m];
}
CF 1968E
不多说,根据样例猜的构造方式
基本就是两个base,然后用对角线填数字就行了
void Solve(){
cin >> n;
if(n == 2){
cout << "1 1\n1 2\n";
}else if(n == 3){
cout << "1 1\n1 2\n2 3\n";
}else{
cout << "1 1\n1 2\n2 4\n";
for(int i=4;i<=n;++i) cout << i << ' ' << i << '\n';
}
}
CF 2019C
题意:
思路:
分组结论题:
将 n 种颜色的物品按上限 c 分组,保证每组内物品颜色不同,至少分组数
max(mx,(sum+c-1)/c)
答案上限为 n,枚举每个牌堆中牌的数量 c,然后将物品按照限制分成至少 M 组,则可以求出补齐 M 组所需要的最少张数,多出的补上整堆
那么至少分多少组?考虑经典结论:将 n 种颜色的物品按上限 c 分组,保证每组内物品颜色不同, 至少分组数为 $max(mx, \lceil \frac{sum}{c} \rceil)$
那么对于每种分组,考虑是否能将剩余所需的用 k 张额外牌补齐,$c \times m - sum_a$
因此,仅需检查 k 是否足够即可,
时间复杂度O(n)
void Solve(){
cin >> n >> k;
int mx = 0, sum = 0;
for(int i=1;i<=n;++i){
cin >> x, mx = max(mx,x), sum += x;
}
int ans = 0; // 每组的最大张数
for(int i=1;i<=n;++i){
int M = max(mx, 1ll*(sum+i-1)/i), need = (1ll*i * M - sum);
if(k < need) continue;
ans = i;
}
cout << ans << '\n';
}
CF 1956C
构造
void Solve(){
cin >> n;
vector<pii> op;
int sum = 0, tot = n*(n+1)/2, now = n;
vector<vector<int>> ans(n+1,vector<int>(n+1));
For(i,1,2*n){
if(i&1){
op.pb({1,now});
For(j,1,n) ans[now][j] = j;
}else{
op.pb({2,now});
For(j,1,n) ans[j][now] = j;
--now;
}
}
For(i,1,n) For(j,1,n){
cerr << ans[i][j] << " \n"[j==n];
sum+=ans[i][j];
}
cout << sum << ' ' << op.size() << '\n';
for(auto &[type, id] : op){
cout << type << ' ' << id << ' ';
For(i,1,n) cout << i << " \n"[i==n];
}
}
CF 1941E
思路:
滑窗 + 单调队列优化DP
外部是定长滑窗取长度为k的区间min,内部是dp[i]:表示以 i 位置结尾的支撑所需要的花费,每个支撑物都是从前一个转移过来就行,前一个取min,我这里当然也是min,
也就不需要对 dp[i] 再取min,否则就是不以 i 为结尾也行,但是题目要求 m 位置必须有,也就这样定义了
发现转移的min就是一个区间最小值,可以用单调队列优化,然后就没了
有点意思,是这个分段该有的题吗?
void Solve(){
cin >> n >> m >> k >> d;
vector<vector<int>> Map(n+1, vector<int>(m+1));
For(i,1,n) For(j,1,m) cin >> Map[i][j];
vector<int> f(n+1, 1e18), g(n+1); // 答案选择的连续 k 行,每行的最小花费
auto DP = [&](vector<int> a) -> int{
vector<int> dp(m+1,INT_MAX);
dp[1] = 1;
// 单调对列维护区间最小值,先将窗口外的元素弹出,然后元素入队,将所有比该元素大的全部弹出,加入该元素
// 这样队列中就是单调递增的一个区间,拿出最小值就是队头元素
deque<int> dq; dq.pb(1);
for(int i=2;i<=m;++i){
while(dq.size() && dq.front() < (i-(d+1))) dq.pop_front();
dp[i] = a[i] + 1 + dp[dq.front()];
while(dq.size() && dp[dq.back()] >= dp[i]) dq.pop_back();
dq.pb(i);
}
// For(i,1,m) cerr << dp[i] << " \n"[i == m];
return dp[m];
};
For(i,1,n) g[i] = DP(Map[i]);
// For(i,1,n) cerr << g[i] << " \n"[i==n];
ll sum = 0;
For(i,1,k) sum += g[i];
ll ans = sum;
For(i,k+1,n){
sum = sum + g[i] - g[i-k];
ans = min(ans,sum);
}
cout << ans << '\n';
}
CF 1932E
思路:
发现就是前缀的所有数字之和,由于结果非常大,所以逐个数字处理,就是模拟手动加法完了
说起来,去年在力扣上也有一个模拟手算加法题,当时完全看不懂,而且题解好傻,不知道为什么
void Solve(){
cin >> n >> s;
vector<int> ans,sum(n+1);
for(int i=0;i<n;++i){
sum[i+1] = sum[i] + s[i] - '0';
}
int now = 0;
while(sum.size()){
now += sum.back(); sum.pop_back();
ans.pb(now % 10);
now /= 10;
}
while(ans.size() && ans.back() == 0) ans.pop_back();
while(ans.size()) cout << ans.back(), ans.pop_back();
cout << '\n';
}
CF 2072F
思路:
首先发现是杨辉三角的系数的奇偶性判断
然后化简组合数公式 $\frac{i\cdot i+1 \dots i-j+1}{j!}$,这个乘积形式最后肯定是一些因子相乘,
只要这个因子中没有 2 就说明是奇数,所以 (f[i] - f[i-j+1] - f[j]) == 0 则说明是奇数,否则就是偶数
DP打表出来 dp[i]:表示 i 的阶乘中因子为 2 的个数
后面这个DP是上课一个同学想出来的qwq,上课还帮我A了一题,哎哎哎
代码:
void init(){
auto get = [&](int x) -> int{
int res = 0;
while(x%2==0) x/=2,res ++;
return res;
};
for(int i=1;i<=1e6;++i){
f[i] = f[i-1] + get(i);
}
}
void Solve(){
cin >> n >> k;
f[0] = 1;
for(int i=0;i<n;++i) cout << ((f[n-1] - f[n-1-i] - f[i] <= 0) ? k : 0 ) << ' ';
cout << '\n';
}
CF 1975D
题意:
最初在树上给了你两个点,A点将白点涂成红色,B点将红点涂成蓝点
每步 A 和 B 移动到相邻节点,且A和B必须同时走一步
求所有点涂成蓝点的最小步数
思路:
- 发现就是以 A 为根节点的欧拉序/ dfs序 作为 base 步数,然后在这基础上加上 B 多出来的花费
- B 先追 A,然后按照 A 的遍历顺序遍历所有点
- 考虑最优化,发现只有一棵子树遍历不需要回到父亲,其他子树都要回到父亲,
所以维护每个儿子不回到父亲遍历需要多少步,回到父亲的步数 = 不回到父亲 + max(dep),回到父亲刚好是最大深度 + 不回到父亲的步数
定义:dp[u]:不回到父亲遍历所有儿子的最少步数
dp[u] = dp[v] + step[v] - (max(step[v]))
发现儿子往父亲的转移跟根节点是一致的!
维护每个儿子往下的最大深度就行了
以 A 做根就好了,然后选择可能的交汇点,路径长度奇数一个点,偶数两个点,只要以这两个交汇点为跟做一次上述的逻辑就能AC了
int Fa[Maxn];
void dfs(int u,int fa){
dep[u] = dep[fa] + 1, Fa[u] = fa;
for(auto v : G[u]){
if(v == fa) continue;
dfs(v,u);
}
}
void dfs2(int u,int fa){
int mx = 0;
for(auto v : G[u]){
if(v == fa) continue;
dfs2(v,u);
int step = mxdep[v] - dep[u]; mx = max(mx, step);
dp[u] += dp[v] + step + 1;
mxdep[u] = max(mxdep[v], mxdep[u]);
}
dp[u] -= mx;
}
void init(){
For(i,1,n) G[i].clear(), mxdep[i] = 0, dp[i] = 0,dep[i] = 0;
}
void Solve(){
cin >> n >> A >> B; init();
For(i,1,n-1) cin >> u >> v, G[u].pb(v), G[v].pb(u);
dfs(A,0); int C = A;
vector<int> todo, path;
for(int x = B; x != A; x = Fa[x]) path.pb(x);
path.pb(A);
reverse(all(path));
m = path.size();
for(int i=0;i<m;++i) if(i+1==m/2 || i+1 == (m+1)/2) todo.pb(path[i]);
// for(auto x : todo ) cerr << x << '\n';
ll ans = 1e18;
for(auto C : todo){
dfs(C,0);
For(i,1,n) dp[i] = 0, mxdep[i] = dep[i];
dfs2(C,0);
ans=min(ans, dp[C] + max(dep[A] - dep[C], dep[B] - dep[C]));
}
cout << ans << '\n';
}
CF 2019E
题意:
每次只能删除一个叶节点,求最少删除次数,使得所有叶子到达根节点的距离相等?
思路:
本质是每个点得存活时间就是该节点到子树深度最大得地方,出了这个区间这个点必须删除
这个信息可以差分维护区间加,得到每个点对深度的贡献之后,找到最大前缀和就行了,就是保留的最多点数
删除最少点就是 n - mx,总数减去保留最多的,就是删掉最少的
void dfs(int u,int fa){
sz[u] = 1;
dep[u] = dep[fa] + 1;
for(auto v : G[u]){
if(v == fa) continue;
dfs(v,u);
sz[u] += sz[v];
}
}
void dfs2(int u,int fa){
mxdep[u] = dep[u];
for(auto v : G[u]){
if(v == fa) continue;
dfs2(v,u);
mxdep[u] = max(mxdep[u], mxdep[v]);
}
}
void Solve(){
cin >> n; For(i,1,n) G[i].clear();
For(i,1,n-1) cin >> u >> v, G[u].pb(v), G[v].pb(u);
dfs(1,0);dfs2(1,0);
int H = *max_element(dep+1,dep+n+1);
vector<int> cnt(H+1), sum(H+2),d(H+3);
For(i,1,n) d[dep[i]]++,d[mxdep[i]+1]--;
int mx = 0, now = 0;
for(int h=1;h<=H;++h){
now += d[h];
mx = max(mx, now);
}
cout << n - mx << '\n';
}
CF 2053D
思路:
就是排序后的配对取值就是答案,修改的时候只有更小值增大才能改变答案
问题在于,修改一次之后,排序中索引改变,配对也发生变化,
只有 $A_x + 1 > A_{x+1}$ 时才会改变,一直 swap 即可
但是,只要找到所有相同的值的最后一个不就不需要swap了吗?
所以只要查一下,改一下完了
注意力惊人!
void Solve(){
cin >> n >> q;
vector<int> a(n+1), b = a, A = a, B = a;
For(i,1,n) cin >> a[i];
For(i,1,n) cin >> b[i];
A = a, B = b;
sort(all(B)), sort(all(A));
ll ans = 1;
For(i,1,n) ans = 1ll*ans*min(A[i],B[i])%Mod;
cout << ans << ' ';
while(q--){
int op, x; cin >> op >> x;
if(op == 1){
int p = upper_bound(all(A), a[x]) - A.begin() - 1;
if(A[p] < B[p]) ans = 1ll*ans * qmi(A[p],Mod-2,Mod) % Mod * (A[p]+1) % Mod;
a[x]++,A[p]++;
}else{
int p = upper_bound(all(B), b[x]) - B.begin() - 1;
if(B[p] < A[p]) ans = 1ll*ans * qmi(B[p],Mod-2,Mod) % Mod * (B[p]+1) % Mod;
b[x]++,B[p]++;
}
cout << ans << ' ';
}
cout << '\n';
}
CF 2065G
思路:
统计三种配对即可,然后组合计数方案
显然:lcm(x,x) = x,则半质数自身就能配对
x | y, lcm(x,y) = y,则为半质数和 x 配对
x 与 y 互质,则相互配对,由于条件限制,最后贡献个数 /= 2
怎么突然有点反胃啊,不想训了突然,一种厌恶感觉,我去
不会给我训吐了?
int f[Maxn];
vector<int> fac[Maxn];
void init(){
for(int i=2;i<=2e5;++i)if(!f[i]){
for(int j=1;j*i<=2e5;++j){
int x=i*j;
while(x%i==0&&f[i*j]!=3)f[i*j]++,x/=i;
fac[i*j].pb(i);
}
}
}
void Solve(){
cin >> n; vector<int> cnt(n+1);
for(int i=1;i<=n;++i) cin >> x, cnt[x]++,cnt[0]+=(f[x]==1);
ll ans = 0, tot = 0;
for(int i=1;i<=n;++i){
if(f[i]==2) ans += 1ll*cnt[i]*(cnt[i]-1)/2+cnt[i]+1ll*cnt[i]*cnt[fac[i][0]] + (fac[i].size()>1?cnt[i]*cnt[fac[i][1]]:0);
else if(f[i] == 1) tot += 1ll*cnt[i]*(cnt[0] - cnt[i]);
}
cout << ans + tot/2 << '\n';
}
CF 2065F
题意:
树上点权 [1,n],对于每个 i,树上是否存在路径使得 i 的出现次数严格大于路径长度的一半
思路:
能够向外两步之内拿到相同值就有可能,否则不可能,直接找数字
父亲的所有相邻节点?
对于每个点,往上看看父亲相邻节点权值个数是否>=2即可
时间复杂度是 O(n)
unordered_map<int,int> cnt[Maxn];
int Fa[Maxn];
void dfs(int u,int fa){
Fa[u] = fa;
for(auto v : G[u]){
if(v == fa) continue;
dfs(v,u);
}
}
void Solve(){
cin >> n; For(i,1,n) G[i].clear(), cnt[i].clear();
vector<int> num(n+1), ans(n+1);
For(i,1,n) cin >> num[i];
For(i,1,n-1) cin >> u >> v, G[u].pb(v), G[v].pb(u), cnt[u][num[v]]++, cnt[v][num[u]]++;
dfs(1,0);
function<void(int,int)> work = [&](int u,int fa)->void{
int val = num[u];
for(auto v : G[u]){
if(v == fa) continue;
work(v,u);
}
if(num[fa] == val) ans[val] = 1;
else if(cnt[fa][val]>=2){
ans[val] = 1;
}
};work(1,0);
// cerr << cnt[2].count(3) << '\n';
For(i,1,n) cout << ans[i];
cout<<'\n';
}
CF 2050F
思路:
就是公差区间 gcd,上个ST表,完了
浪费时间
template <typename T>
class ST{
public:
const int n;
vector<vector<T>> st;
ST(int n = 0, vector<T> &a = {}) : n(n){
st = vector(n + 1, vector<T>(22 + 1));
build(n, a);
}
inline T get(const T &x, const T &y){
return gcd(x, y);
}
void build(int n, vector<T> &a){
for(int i = 1; i <= n; i++){
st[i][0] = a[i];
}
for(int j = 1, t = 2; t <= n; j++, t <<= 1){
for(int i = 1; i <= n; i++){
if(i + t - 1 > n) break;
st[i][j] = get(st[i][j - 1], st[i + (t >> 1)][j - 1]);
}
}
}
inline T find(int l, int r){
int t = log(r - l + 1) / log(2);
return get(st[l][t], st[r - (1 << t) + 1][t]);
}
};
void Solve(){
cin >> n >> q;
vector<int> num(n+1), d(n+1);
For(i,1,n) cin >> num[i];
For(i,1,n-1) d[i] = abs(num[i+1]-num[i]);
ST<int> st(n,d);
while(q--){
int l,r; cin >> l >> r;
if(l == r) cout << 0 << ' ';
else cout << st.find(l,r-1) << ' ';
}
cout << '\n';
}
CF 2052F
思路:
就是表示 4 种状态的DP,加上种类数
然后就是恐怖的位掩码表示, 00 表示该列填满, 01 表示下面格子需要填,10 表示上面格子需要填, 11 表示两个都要填
枚举状态就好了,非法状态是 (mask & (~num[i]) != 0) 表示放的格子与原来位置存在冲突,
注意,枚举的是横着放的格子,所以 11 的时候可以竖着填,直接填满,转移到下一个位置即可
// 00 10 01 11 从前面往后延伸的,01 上 10 下 11 两排都放 / 00 竖着放
void Solve(){
cin >> n >> a >> b;
vector<int> num(n+1),dp(4);
for(int i=0;i<n;++i){
num[i+1] += (a[i] == '.' ? 1 : 0);
num[i+1] += (b[i] == '.' ? 2 : 0);
}
dp[0] = 1; // 横过来放的可行方案数,1
for(int i=1;i<=n;++i){
vector<int> ndp(4);
for(int j=0;j<4;++j){
if ((j&(~num[i]))) continue;
int now = num[i]^j; // 将前面多出来的格子延伸到这里之后的需求状态:now
ndp[now] += dp[j], ndp[now] = min(2,ndp[now]);
if(now == 3) ndp[0] += dp[j], ndp[0] = min(2,ndp[0]);
}
dp.swap(ndp);
}
int ans = dp[0];
if(ans == 0) puts("None");
else if(ans == 1) puts("Unique");
else puts("Multiple");
}
CF 2030D
核心观察:
只要 $s_i = L$ 且 $s_{i+1} = R$,则 $1 \sim i$ 左边的所有数字都无法到右边,右边的所有数字无法到左边,此时只要 $1 \sim i$ 的所有数字存在不再左边的数字就说明这个分界线对答案有贡献,只要这次分界线非 0,就输出 NO
考虑修改,如果破坏了这类分界线,或者生成了这类分界线都需要统计
分界线的生成为从 $1 \sim i$ 包含了所有种类数的数字,
我们可以预处理一个 bool 数组完成这件事
void Solve(){
cin >> n >> q;
vector<int> num(n+1), b(n+1);
int mx = 0, mn = n;
For(i,1,n) {
cin >> x, num[i] = x, mx = max(mx, x), mn = min(mn,x);
if(!(mx == i && mn == 1)) b[i] = 1;
}
// For(i,1,n) cerr << b[i] << " \n"[i==n];
set<int> vis;
cin >> s, s = " " + s;
for(int i=1;i<n;++i) if(s[i] == 'L' && s[i+1] == 'R' && b[i]) vis.insert(i);
while(q--){
cin >> x;
if(x>1) vis.erase(x-1);
vis.erase(x);
s[x] = (s[x] == 'L' ? 'R' : 'L');
if(x<n && s[x] == 'L' && s[x+1] == 'R' && b[x]) vis.insert(x);
if(x>1 && s[x] == 'R' && s[x-1] == 'L' && b[x-1]) vis.insert(x-1);
puts(vis.size() ? "NO" :"YES");
}
}
CF 1986E
最后只会有一个奇数桶,因为是 % k!
那 1 怎么办? 那就是全部放到 0 桶子,不是 k+1 !
如果题目描述中存在对任意位置进行恰好一次操作等字眼,通常考虑可以枚举对哪一个字符进行操作
有至多一个奇数段才有解
偶数个,从小到大两两配对,奇数个枚举删除位置算最小贡献
// 恰好一次操作求花费数,常见套路:枚举这个操作的位置
int work(vector<int> a){
int m = a.size()-1;
// cerr << m << '\n';
vector<int> pre(m+1), suf(m+2);
for(int i=1, flg = -1;i <= m; ++i, flg = -flg) pre[i] = pre[i-1] + a[i]*flg;
for(int i=m, flg = 1;i >= 1; --i, flg = -flg) suf[i] = suf[i+1] + a[i]*flg;
ll res = 1e18;
for(int i=1;i<=m;++i){
ll cost = pre[i-1] + suf[i+1];
res = min(res,cost);
}
return res;
}
void Solve(){
cin >> n >> k;
vector<int> num(n+1);
For(i,1,n) cin >> num[i];
sort(all(num),[&](const int a,const int b){
if(a%k == b%k) return a<b;
return a%k < b%k;
});
// For(i,1,n) cerr << num[i] << " \n"[i==n];
int cnt = 0, ans = 0, l = -1, r = -1;
for(int i=1;i<=n;++i){
int j = i;
while(j<=n && (num[j]%k == num[i]%k)) ++j;
int len = j-i;
// cerr << len << '\n';
if(len&1) {
if(cnt<1) l = i, r = j-1;
++cnt;
}
i = j-1;
}
if(cnt > 1) return cout << "-1\n", void();
vector<int> a{0};
if(n&1){
for(int i=1,flg=-1;i< l;++i,flg=-flg) ans += num[i]*flg;
for(int i=r+1, flg = -1; i<=n; ++i, flg=-flg) ans += num[i]*flg;
For(i,l,r) a.pb(num[i]);
ans += work(a);
}else{
for(int i=1,flg=-1;i<=n;++i,flg=-flg) ans += num[i]*flg;
}
// cerr << l << ' ' << r << '\n';
cout << ans/k << '\n';
}
CF 2029C
简单线性DP
题意:
初始 x = 0, 若 ai > x,则 ++x, 若 ai = x, 则 x += 0,若 ai < x,则 x += -1
删除一段区间,求x最后的最大值
思路:
f[0/1/2] 表示第 i 个位置没有删除,正在删除,删除完毕的最大值
ans = max(f[1],f[2])
nf[0] = get(f[0], num[i]);
nf[1] = max(f[1], f[0]);
nf[2] = max(get(f[2], num[i]), get(f[1], num[i]));
void Solve(){
cin >> n; vector<int> num(n+1);
For(i,1,n) cin >> num[i];
auto get = [&](int a,int b){
return (a == b ? a : a < b? a+1 : a-1);
};
vector<int> f(3);
f[1] = f[2] = -1e18;
for(int i=1;i<=n;++i){
vector<int> nf(3);
nf[0] = get(f[0], num[i]);
nf[1] = max(f[1], f[0]);
nf[2] = max(get(f[2], num[i]), get(f[1], num[i]));
nf.swap(f);
}
cout << max(f[1], f[2]) << '\n';
}
CF 2039D
思路:
求出每个数的质因子个数表,如果存在cnt[i] >= m,则无解
否则,从大到小按照质因子个数填表,直接按质因子个数逐个减少即可
struct Seive{
int n;
vector<int> table,cnt,spf; //质数表,质因子个数,最小质因子
Seive(){}
Seive(int n):n(n),table(n+10),cnt(n+10,0),spf(n+10,0){
prime_table(n,table);
}
void prime_table(int n, vector<int> &table){
vector<int> prime;table.clear();table.resize(n+1,0);
table[0] = table[1] = 1;//1 0 初始化为合数
for(int i=2;i<=n;i++){
if(!table[i])spf[i]=i,prime.push_back(i);
for(auto &p : prime){
if(p*i > n)break;
table[i*p] = 1,spf[i*p]=p;//标记合数
if(i%p == 0) break;
}
cnt[i]=cnt[i/spf[i]]+1; //dp转移得到质因子总个数(少了一个最小质因子,转移过来)
}
for(auto &x : table) x^=1;//反转质数和合数直接判断
}
vector<int> fac(int x){
int t = x;
vector<int> res;
for(int i=2;i*i<=x;++i){
if(x % i == 0){
res.pb(i);
while(x%i==0) x/=i;
}
}
if(x>1) res.pb(x);
return res;
}
}seive(100000);
void Solve(){
cin >> n >> m;
vector<int> num(m+1);
For(i,1,m) cin >> num[i];
For(i,1,n) if(seive.cnt[i] >= m) return cout << "-1\n",void();
For(i,1,n){
cout << num[m-seive.cnt[i]] << " \n"[i==n];
}
}
CF 2009F
思路:
就是前缀和的问题,但是处理区间和好恶心,就是那个下标处理起来好烦人
void Solve(){
cin >> n >> q;
vector<int> num(2*n+1), sum(2*n+1);
int tot = 0;
For(i,1,n) cin >> x, num[i] = num[i+n] = x, tot += x;
For(i,1,n*2) sum[i] = sum[i-1] + num[i];
auto work = [&](int nid, int qid){
int r = nid + qid - 1 - (nid - 1)*n, l = nid;
// cerr << l <<' ' << r << '\n';
return sum[r] - sum[l-1];
};
// cerr << tot << '\n';
while(q--){
int ql,qr, idl, idr;
cin >> ql >> qr, idl = ql/n + (ql%n != 0), idr = qr/n + (qr%n != 0);
int ans = max(0ll, idr - idl - 1)*tot;
// cerr << idl << ' ' << idr << '\n';
if(idl == idr){
int nl = idl + ql - 1 - (idl - 1)*n, nr = idr + qr - 1 - (idr - 1)*n;
// cerr << nl << ' ' << nr << '\n';
ans += sum[nr] - sum[nl-1];
}else{
if(qr <= n) ans += sum[qr] - sum[ql-1];
else if(ql <= n){
ans += sum[n] - sum[ql-1] + work(idr, qr);
// cerr << sum[n] - sum[ql-1] << '\n';
}else{
ans += tot - work(idl,ql-1) + work(idr,qr);
}
}
cout << ans << '\n';
}
}
CF 1983D
思路:
交换就要想到逆序对
由于每次交换逆序对的奇偶性一定会发生改变,所以两个序列逆序对奇偶性相同则可以操作成相同数组
代码:
struct BIT {
int n;
vector<int> f;
BIT(int _n):n(_n),f(n+1,0){}
void add(int i,int v){for(;i<=n;i+=i&-i)f[i]+=v;}
int sum(int i){int s=0;for(;i>0;i-=i&-i)s+=f[i];return s;}
int query(int l,int r){return l>r?0:sum(r)-sum(l-1);}
};
int work(vector<int> a, vector<int> lsh){
auto get = [&](int x){
return lower_bound(all(lsh), x) - lsh.begin() + 1;
};
BIT bit(n+1);
int res = 0;
for(int i=n-1;i>=0;--i){
int id = get(a[i]);
res += bit.sum(id);
bit.add(id,1);
}
return res;
}
void Solve(){
cin >> n;
vector<int> a(n),b(n),c,d;
vector<int> lsh;
for(auto &x : a) cin >> x, lsh.pb(x);
for(auto &x : b) cin >> x;
c = a, d = b;
sort(all(c)),sort(all(d));
if(c!=d) return puts("NO"),void();
sort(all(lsh));
int cnta = work(a,lsh), cntb = work(b,lsh);
// cerr << cnta << ' ' << cntb << '\n';
puts(cnta % 2 == cntb % 2 ? "YES" : "NO");
}
CF 1955E
思路:
差分,边枚举边记录终点,
直接用前缀和
枚举长度,枚举1的位置翻转,模拟操作即可
维护差分,边求和边记录差分,只要记录终点结束差分位置即可
// 枚举长度,枚举起点
bool ck(int len){
vector<int> need(n+1);
for(int i=1;i<=n;++i) need[i] = (s[i-1] - '0');
// For(i,1,n) cerr << need[i] << " \n"[i==n];
// 差分,一边翻转,一边记录前缀和即可
vector<int> d(n+5);
int now = 0;
for(int i=1;i<=n;++i){
now ^= d[i];
int bit = need[i]^now;
if(bit==0){
if(i+len-1>n)return 0;
now^=1; // i位置翻转一次
d[i+len]^=1; // len 之后影响结束,记录一次
}
}
return 1;
}
void Solve(){
cin >> n >> s;
int ans = 1;
// cerr << ck(3) << '\n';
for(int len=n;len>=2;--len){
if(ck(len)) {ans=len;break;}
}
cout<<ans<<'\n';
}
CF 2006A
思路:
https://www.luogu.com.cn/article/1efoz8u0
博弈算得分,可以用未知得分 /2,计算到在阻止情况下对答案的贡献
trick:01 = 10 串的数量,则字符串首部和尾部字符相同即可
手玩样例可得出一个结论:若根节点颜色和叶子节点相同,则叶子结点权值为 0,否则不为 0。这也很好理解:可以把根到叶子的字符串比作爬山的过程,1 就是山顶,0 就是山谷,那么 01 就是上山,10 就是下山。如果你想让上山和下山的次数相等,那么你爬完这座山过后的高度显然要和你初始的高度一样。
我们先给树做一遍 dfs,cnt0 表示叶节点 0 的出现个数,cnt1 表示 1 出现个数,cnt2 表示问号的出现个数,si 表示第 i 个节点的颜色。接着就是分类讨论。如根节点已被染色,那么 Iris 肯定会染和根节点相反的颜色,Dora 会染和根节点相同的颜色。由于 Iris 先操作,因此答案要向上取整,也就是 cnt1−s1+⌊2cnt2+1⌋。
如果根节点未被染色,由于 Iris 先操作,那么他肯定先把根节点染成叶子结点出现次数更多的颜色。但是叶子结点现在变成 Dora 先操作了。因此 Dora 会染和根相同的颜色来减少答案。因此答案就是 max(cnt0,cnt1)+⌊2cnt2⌋。
但是其中有一个特例,如果 cnt0=cnt1,那么二人无论谁先染色,都对自己不利。因为一旦染完就换对方走了,对方就会利用你染的颜色来改变答案。因此他们就会先染除了根和叶子之外还没染色的节点。因此我们在 dfs 时多统计一个 cnt3,表示除了根和叶子之外还没染色的节点个数。如果 cnt3 为奇数,那么全部染色完之后就是 Dora 先操作。答案就是 max(cnt0,cnt1)+⌊2cnt2+1⌋,否则答案就是 max(cnt0,cnt1)+⌊2cnt2⌋。
void dfs(int u,int fa){
if(G[u].size() == 1 && u!=1){
if(s[u] == '?') cnt[2]++;
else cnt[s[u]-'0']++;
}else if(u!=1 && s[u] == '?') cnt[3]++; // 不是 根和叶子的没染色节点数
for(auto v : G[u]){
if(v == fa) continue;
dfs(v,u);
}
}
void init(){
For(i,1,n) G[i].clear();
cnt.assign(4,0);
}
void solve(){
cin >> n;init();
For(i,1,n-1) cin>>u>>v,G[u].pb(v),G[v].pb(u);
cin >> s, s = " " + s;
dfs(1,0);
if(s[1] == '?'){
if(cnt[1]==cnt[0])cout << max(cnt[0], cnt[1]) + (cnt[2] + (cnt[3]&1))/2<<'\n';
else cout << max(cnt[0], cnt[1]) + cnt[2]/2 << '\n';
}else cout << cnt[1-(s[1]-'0')] + (cnt[2]+1)/2 << '\n';
}

浙公网安备 33010602011771号