A

计数DP

1表示看到白天,0表示看到晚上,-1表示看到不确定。给个序列,问所有把-1变成0或1的方案中,白天天数的和?

首先如果只是求方案数是好求的,但是这里每个方案都有贡献,要求的是所有方案的贡献和。这种一般就维护两个dp数组,一个是贡献和,一个是方案数,方案数正常转移,什么时候能产生贡献了,再把方案数加入答案。

这里能产生贡献的地方就是前一个时0,当前是1,这意味着开始了新的一个白天,我们把计算出来的方案数加上

#include<bits/stdc++.h>
using namespace std;
const int N=5e5+5;
const long long mod=998244353;
long long n,t;
long long a[N],dp[N][5],f[N][5];
int main(){
    scanf("%d",&t);
    while(t--){
        scanf("%d",&n);
        for(int i=0;i<=n;i++){
            for(int j=0;j<=1;j++)dp[i][j]=f[i][j]=0;
        }
        f[0][0]=1;
        for(int i=1;i<=n;i++){
            scanf("%lld",&a[i]);
            if(a[i]==1){
                dp[i][1]=(dp[i-1][0]+dp[i-1][1]+f[i-1][0])%mod;
                f[i][1]=(f[i-1][0]+f[i-1][1])%mod;
            }
            else if(a[i]==0){
                dp[i][0]=(dp[i-1][0]+dp[i-1][1])%mod;
                f[i][0]=(f[i-1][0]+f[i-1][1])%mod;
            }
            else {
                dp[i][1]=(dp[i-1][0]+dp[i-1][1]+f[i-1][0])%mod;
                dp[i][0]=(dp[i-1][0]+dp[i-1][1])%mod;
                f[i][1]=f[i][0]=(f[i-1][0]+f[i-1][1])%mod;
            }
        }
        printf("%lld\n",(dp[n][0]+dp[n][1])%mod);
        
    }
    return 0;
}

F

贪心,模拟

一个环上多个起火点,每秒会往相邻一个格子蔓延,t秒后消防队才回来,你可以选一个点的树提前消灭掉,这样火就不能从这一点蔓延了,问最优情况下,消防队来的时候还剩多少点没着火?

多个起火点?环?不好想?

先简单点,一条链上,只有一个起火点,那显然我们只可能在起火点两侧选一个点阻断,这样能救的点是最多的,至于选哪个点,要看哪边的树多。

多个点?可能的答案点仍然是每个初始起火点的左右两侧,具体选哪个,看所有连续0段的长度,应该选最长的0段的端点,这样可能救的树最多

环怎么办?因为至少有一个起火点,也就是1,那么我们不妨从第一个1开始考虑,这个1左侧的0,可以接到最右侧,因为这是环。

如何计算答案,找到最长0后,除了这一段答案都是,也就是长度的0被从两端开始烧了秒,剩下这么多,可能是负数,要和0取max。最长0这一段,有一个端点来的火被我们阻断了,相当于只从一端开始着火,即可,仍然和0取max

#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=5e5+5;
const long long mod=998244353;
int T;
signed main(){
    cin>>T;
    while(T--){
        int n,t;
        cin>>n>>t;
        
        string s;
        cin>>s;
        s=' '+s;
        
        int pre=0;
        for(int i=1;i<=n;i++){
            if(s[i]=='1'){
                break;
            }
            else{
                pre++;
            }
        }
        for(int i=0;i<pre;i++){
            s+='0';
        }
        
        int ans=0;
        int len=0;
        int mx=0;
        for(int i=pre+1;i<=n+pre;i++){
            if(s[i]=='0'){
                len++;
            }
            else{
                mx=max(mx,len);
                ans+=max(0ll,len-2*t);
                len=0;
            }
        }
        ans+=max(0ll,len-2*t);
        mx=max(mx,len);
        
        ans-=max(0ll,mx-2*t);
        ans+=max(0ll,mx-t-1);
               
        cout<<ans<<'\n';
    }
    return 0;
}

G

几何

给一个凸多边形,饶益格中心转,问最多转多少角度后,扫过的面积不再增大?

旋转问题一个关键点是考虑中心在多边形的内外。

如果在多边形内,可以看成多个半径在画圆,那么面积增长取决于最长半径什么时候画出一个完整圆,如果最长半径还没画完,即使小半径画完一圈了,面积仍然在增长。最长半径只有一个的话就是画一圈,如果有多个的话,手玩可以发现就是这些半径之间的最大夹角,因为一块转动的话,大夹角扫过了,小夹角肯定也扫过了。找到最大半径,极角序排序然后扫一遍,计算相邻的差值即可,注意这是个循环数组,第一个和最后一个也构成一个角。不过点给出的顺序就是逆时针了,也不用极角序排序了。

如果中心在多边形外部,关于最大半径的讨论还是一样的,但是最小半径也会产生影响,因为旋转时画的是个圆环,外圈边界时最大半径决定的,内圈半径是最小半径决定的,所以我们还要看最小半径有几个。

关键点来了,最小半径,也就是距离旋转中心最近的点一定只有一个,不信可以手玩,因为这是凸多边形,(凹多边形可能有多个)。那么根据我们上面对于最大半径的讨论,只有一个的话肯定要转一整圈,答案永远是2pi

实现上的一个坑点是给出点,怎么求和x正方向的夹角?这可以用atan函数,但是注意值域。需要加个偏移量。另外跨一四象限的夹角计算也需要偏移量,无脑办法就是大于2pi小于0,都无脑加减2pi

#include <bits/stdc++.h>
using namespace std;
using ll = long long;
using db = ll;

constexpr long double PI = acos(-1);
constexpr double eps = 1e-8;

struct P {
    db x, y;
    P(db x = 0, db y = 0) : x(x), y(y) {}

    P operator-(const P &o) const { return P(x - o.x, y - o.y); }
    db operator*(const P &o) const { return (x * o.x + y * o.y); }
    db operator^(const P &o) const { return x * o.y - y * o.x; }
    long double theta() {
        long double res = atan2((long double)y, (long double)x);
        if (res < 0) res += 2 * PI;
        return res;
    }
};

db abs2(const P &p) { return p * p; }

using Poly = vector<P>;

bool contain(const Poly &g, const P &p) {
    bool inPoly = false;
    for (size_t i = 0; i < g.size(); i++) {
        P a = g[i] - p, b = g[(i + 1) % g.size()] - p;
        if (abs(a ^ b) == 0 and a * b <= 0) return true;
        if (a.y > b.y) swap(a, b);
        if (a.y <= 0 and 0 < b.y and (a ^ b) > 0) inPoly = !inPoly;
    }
    return inPoly;
}

long double calc(vector<long double> &a) {
    if (a.size() == 1) {
        return 2 * PI;
    }

    long double ans = 0;
    for (size_t i = 0; i + 1 < a.size(); i++) {
        long double tmp = a[i + 1] - a[i];
        while (tmp < 0) tmp += 2 * PI;
        while (tmp > 2 * PI) tmp -= 2 * PI;
        ans = max(ans, tmp);
    }
    long double tmp = a.front() + 2 * PI - a.back();
    while (tmp < 0) tmp += 2 * PI;
    while (tmp > 2 * PI) tmp -= 2 * PI;
    ans = max(ans, tmp);

    return ans;
}

void solve() {
    ll n, x, y;
    cin >> n >> x >> y;
    P c(x, y);
    Poly g(n);
    ll maxD = 0;
    ll minD = LONG_LONG_MAX;
    for (int i = 0; i < n; i++) {
        cin >> g[i].x >> g[i].y;
        g[i] = g[i] - c;
        maxD = max(maxD, abs2(g[i]));
        minD = min(minD, abs2(g[i]));
    }

    vector<long double> a, b;
    a.reserve(n);
    b.reserve(n);
    for (int i = 0; i < n; i++) {
        if (abs2(g[i]) == maxD) {
            a.push_back(g[i].theta());
        }
        if (abs2(g[i]) == minD) {
            b.push_back(g[i].theta());
        }
    }
    
    long double ans;
    if (contain(g, P(0, 0))) {
        ans = calc(a);
    } else {
        ans = 2*PI;
    }
    cout << fixed << setprecision(15) << ans << "\n";
}

int main() {
    ios::sync_with_stdio(false), cin.tie(nullptr);
    int T;
    cin >> T;
    while (T--) {
        solve();
    }
}

H

李超线段树 最短路

一张图,每条边每次升级可以边权减去,现在给k次机会升级,问1-n最短路?

注意到满足每条边都有,也就是升级次数全用在这一条边上也不会浪费。所以一定存在一个最优解,k次升级都用在了同一个边上。那么我们可以枚举用在了哪条边上,如果用在了边上,那么此时的最短路就是

那么可以先预处理出来1和n出发到所有点的最短路,原式可以转化为,有多个k的询问,每次询问要在m个这样的式子里取最小值。

这是啥?你把每个式子看成一个直线,这就是多条直线,在给定x坐标处的最小值?李超树板子即可。

注意这里是直线,直接把插入区间等于李超树定义域即可,或者只要线段定义域比k范围大即可

#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = a; i <= b; i++)
#define per(i, b, a) for(int i = b; i >= a; i--)
#define endl "\n"
using namespace std;
using ll = long long;
using ull = unsigned long long;
using ld = long double;
using pll = pair<ll, ll>;
using graph = vector<vector<pll>>;
using ugraph = vector<vector<int>>;
constexpr int MAXN = 2E5 + 5;
constexpr double eps = 1E-8;
constexpr ll INF = 1E18;

vector<ll> dijkstra(graph &g, ll s) {
    vector<ll> dist(g.size(), INF);
    priority_queue<pll, vector<pll>, greater<pll>> pq;
    dist[s] = 0;
    pq.emplace(0, s);
    while (!pq.empty()) {
        auto [d, u] = pq.top(); pq.pop();
        if (dist[u] < d) continue;
        for (auto [w, v] : g[u]) {
            if (dist[v] > dist[u] + w) {
                dist[v] = dist[u] + w;
                pq.emplace(dist[v], v);
            }
        }
    }
    return dist;
}

struct LiChao {
    struct Line { 
        ll k, b; 
        ll eval(ll x) const { return k*x + b; } 
    };
    struct Node { 
        Line ln; 
        Node *l, *r; 
        Node(Line v): ln(v), l(nullptr), r(nullptr) {} 
    };

    ll L, R;
    Node* root;
    LiChao(ll _L, ll _R): L(_L), R(_R), root(nullptr) {}

    void insert(Line nw, ll l, ll r, Node*& o, ll seg_l, ll seg_r) {
        if (!o) { o = new Node(nw); return; }
        
        ll m = (l + r) >> 1;
        
        // 如果线段完全不在当前区间
        if (seg_r < l || seg_l > r) return;
        
        // 如果线段完全覆盖当前区间
        if (seg_l <= l && r <= seg_r) {
            bool left = nw.eval(l) < o->ln.eval(l);
            bool mid = nw.eval(m) < o->ln.eval(m);
            bool right = nw.eval(r) < o->ln.eval(r);
            
            if (left && right) {
                swap(nw, o->ln);
                return;
            }
            if (!left && !right) return;
            
            if (mid) swap(nw, o->ln);
            
            if (left != mid) insert(nw, l, m, o->l, seg_l, seg_r);
            if (right != mid) insert(nw, m+1, r, o->r, seg_l, seg_r);
            return;
        }
        
        // 线段部分覆盖当前区间
        if (seg_l <= m) insert(nw, l, m, o->l, seg_l, seg_r);
        if (seg_r > m) insert(nw, m+1, r, o->r, seg_l, seg_r);
    }

    void add_segment(ll k, ll b, ll left, ll right) { 
        insert({k, b}, L, R, root, left, right); 
    }

    ll query(ll x, ll l, ll r, Node* o) const {
        if (!o) return LLONG_MAX;
        ll m = (l + r) >> 1, res = o->ln.eval(x);
        if (l == r) return res;
        return min(res, x <= m ? query(x, l, m, o->l)
                              : query(x, m+1, r, o->r));
    }

    ll get(ll x) const { 
        return query(x, L, R, root); 
    }
};

void solve() {
    int n, m; cin >> n >> m;
    vector<tuple<int, int, ll, ll>> edges(m);
    graph g(n + 1), h(n + 1);
    for (auto &[u, v, t, w] : edges) {
        cin >> u >> v >> t >> w;
        g[u].emplace_back(t, v);
        h[v].emplace_back(t, u);
    }

    auto d1 = dijkstra(g, 1);
    auto d2 = dijkstra(h, n);
    vector<ll> dist(m);
    for (int i = 0; i < m; i++) {
        auto &[u, v, t, w] = edges[i];
        dist[i] = d1[u] + d2[v] + t;
    }
    
    // min{dist_i - w_i * k}
    LiChao seg(0, 1'000'000'000);
    for (int i = 0; i < m; i++) {
        auto &[u, v, t, w] = edges[i];
        seg.add_segment(-w, dist[i],1,100'000'000);
//         seg.add(-w,dist[i]);
    }

    int q; cin >> q;
    for (int i = 0; i < q; i++) {
        int k; cin >> k;
        cout << seg.get(k) << "\n";
    }
}

int main() {
    ios::sync_with_stdio(false), cin.tie(nullptr);
    int T; cin >> T;
    for (int ttt = 1; ttt <= T; ++ttt) {
        solve();
    }
    return 0;
}

L

图论 数学

偶数个人,每个人都有喜欢的人,每个人喜欢的人互不相同。两个人可以结婚条件是至少一个人喜欢另一个人。现在要删掉两个人,删掉之后剩余人都能结婚,问多少种结婚方案,注意不是删除方案,同一个删除方案可能结婚方式不同

喜欢虽然是单向的,但只要一个人喜欢另一个人就可以结婚,在结婚的意义上这其实是个无向图,a喜欢b,ab就可以结婚,有一个无向边。注意到n个人n条边,这是个类似基环森林。并且每个人喜欢的人互不相同,所以环上不能长出来树,如果环上点能长出树,说明要么根由两个人喜欢,要么他喜欢两个人,都是不可能的。

所以构成的图就是一堆环。

我们要删除点,注意到长度为偶数的环删掉奇数个点无解,剩下奇数个人肯定有个人没法结婚,所以要么不删要么删除两个。长度为奇数的环需要删掉奇数个点,也就是必须删除一个点。

可以看到奇数环必须删点,而我们只有两次机会,所以奇数环个数决定了是否有解。 如果奇数环有超过2个就无解,

恰好两个就把两次机会分别给这两个环,每个奇数环上所有点都可以删除,方案数是两个奇环大小乘积,剩余偶数环不删,但是每个长度不为2的偶数环的结婚方案也有两种,所以还要乘上,是长度不为2的偶数环个数。

恰好一个奇数环,用来一个名额,但是剩下一次机会只能去删偶数环了,而偶数环不能删1个,所以此时也无解。

0个奇数环,两次机会可以给一个偶数环,枚举给哪个,这些方案之间是加法。确定一个环了,删除两个位置后得到俩链,这俩链必须都是偶数长度,否则会有人无法结婚,删除方案可以考虑容斥。然后剩余没删除的偶数环算法还是,这里需要考虑这个枚举的环是不是长度大于2,是的话

#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = a; i <= b; i++)
#define per(i, b, a) for(int i = b; i >= a; i--)
#define endl "\n"
#define int long long
using namespace std;
using ll = long long;
using ull = unsigned long long;
using ld = long double;
using pii = pair<int, int>;
using graph = vector<vector<pii>>;
using ugraph = vector<vector<int>>;
constexpr int MAXN = 2E5 + 5;
constexpr double eps = 1E-8;
constexpr ll INF = 1E18;
const int mod=998244353;

constexpr int B = 63;
int p2[505050];

void solve() {
    int n;
    cin>>n;
    
    vector<int>f(n+1),sz(n+1,1);
    for(int i=1;i<=n;i++){
        f[i]=i;
    }
    
    auto &&find=[&](auto &&find,int x)->int{
        if(f[x]==x)return x;
        return f[x]=find(find,f[x]);
    };
    for(int i=1;i<=n;i++){
        int x;
        cin>>x;
        int f1=find(find,i),f2=find(find,x);
        
        if(f1!=f2){
            f[f1]=f2;
            sz[f2]+=sz[f1];
        }
    }
    int cnt0=0,cnt1=0,cnt2=0;
    vector<int>a,b;
    for(int i=1;i<=n;i++){
        if(find(find,i)==i){
            if(sz[i]%2)cnt1++,a.push_back(sz[i]);
            else{
                cnt0++;
                if(sz[i]>2){
                    cnt2++;
                }
                b.push_back(sz[i]);
            }
        }
    }
    
    if(cnt1==0){
        int ans=0;
        for(int x:b){
            int t=x*(x-1)/2;
            t%=mod;
            t-=x/2*(x/2-1);
            t=(t%mod+mod)%mod;
            
            int c=cnt2;
            if(x>2){
                c--;
            }
            t=t*p2[c]%mod;
            
            ans=(ans+t)%mod;
        }
        cout<<ans<<'\n';
    }
    else if(cnt1==2){
        int ans=a[0]*a[1]%mod;
        ans=ans*p2[cnt2]%mod;
        cout<<ans<<'\n';
    }
    else{
        cout<<0<<'\n';
    }
}


signed main() {
    ios::sync_with_stdio(false), cin.tie(nullptr);
    p2[0]=1;
    for(int i=1;i<=500000;i++){
        p2[i]=p2[i-1]*2%mod;
    }
    
    int T; cin >> T;
    for (int ttt = 1; ttt <= T; ++ttt) {
        solve();
    }
    return 0;
}