ICPC2023南京 A思维+DFS K背包+贪心 L贪心+模拟

A

dfs 思维

和题解的做法略有不同。

每次可以控制所有袋鼠一块移动,移动到洞口或者出界都会淘汰,问对于每个袋鼠,是否存在一个操作序列让它活到最后?

注意到每个人都能移动到所在连通块内的任意位置,所以连通块内的每个人要么都能活到最后,要么都不能。(如果有一个人能活到最后,其他人先走到这个人的位置,然后做同样的操作也能活到最后)。所以我们可以以连通块为单位思考,对于每个联通块,能活到最后的条件是,不存在另一个连通块,是它的超集,或者说能完全包住他。因为如果存在一个超集,这个联通块不管怎么动,超集里的元素总能跟着动,要么都出局,要么都不出局,不能让子集内的人成为活到最后的一个人。

所以问题转化成对于每个联通块,判断是否存在另一个块是他的超集?这里涉及到形状,很难直接判断超集。最后乱搞出来一种方法是,对于每个联通快,任选一个点作为代表(块内每个点都是平等的),然后让这个点在块内做dfsdfsdfs,同时带动其它所有人一起移动,也就是其它所有人的移动都要和这个代表完全相同,然后dfsdfsdfs遍历完连通块内所有位置后,看其它所有人是否都被淘汰,如果是就说明这个连通块不存在一个超集,是合法的,把他的大小计入答案。

这里的dfsdfsdfs不仅包含递归进入的移动,还包含回溯的移动。形式化来说就是我们在这个连通块的dfsdfsdfs生成树上进行了一个欧拉序列的访问。如果不包含这个回溯就会wawawa,猜测是因为,回溯的移动在我们实际玩这个游戏的时候也是要做的,并且这部分移动也可能会淘汰一些人。

#include <bits/stdc++.h>
#define int long long
using namespace std;

int dx[4]={1,0,-1,0},dy[4]={0,-1,0,1};
void solve() {
    int n,m;
    cin>>n>>m;
    vector<vector<int>>a(n+1,vector<int>(m+1));
    vector<vector<int>>vis(n+1,vector<int>(m+1));
    vector<vector<int>>pos(n+1,vector<int>(m+1));
    vector<pair<int,int>>p;
    vector<int>live;

    int cnt=0;
    for(int i=1;i<=n;i++){
        for(int j=1;j<=m;j++){
            char c;
            cin>>c;
            if(c=='.'){
                a[i][j]=1;
                p.push_back({i,j});
                pos[i][j]=cnt++;
                live.push_back(1);
            }
        }
    }
    bool ok=0;
    int curp;
    auto &&dfs=[&](auto &&dfs,int x,int y)->int{
        int res=1;
        vis[x][y]=1;
        int cnt=0;
        for(int i=0;i<(int)p.size();i++){
            if(i==curp)continue;
            if(!live[i]){
                cnt++;
            }
        }
        if(cnt==(int)p.size()-1){
            ok=1;
        }
        // vector<int>copy;
        // copy=live;
        for(int i=0;i<4;i++){
            int xx=x+dx[i],yy=y+dy[i];
            if(xx<1||xx>n||yy<1||yy>m||!a[xx][yy]||vis[xx][yy])continue;

            int j=0;
            for(auto &[x,y]:p){
                x+=dx[i];
                y+=dy[i];
                if(live[j]){
                    if(x<1||x>n||y<1||y>m||!a[x][y]){
                        live[j]=0;
                    }
                }
                j++;
            }
            res+=dfs(dfs,xx,yy);

            for(auto &[x,y]:p){
                x-=dx[i];
                y-=dy[i];
            }
            // live=copy;
        }
        return res;
    };

    int ans=0;
    for(int i=1;i<=n;i++){
        for(int j=1;j<=m;j++){
            if(a[i][j]&&!vis[i][j]){
                ok=0;
                curp=pos[i][j];
                vector<int>copy;
                copy=live;
                int sz=dfs(dfs,i,j);
                live=copy;
                // cout<<i<<' '<<j<<' '<<ok<<' '<<sz<<'\n';
                if(ok){
                    ans+=sz;
                }
            }
        }
    }
    cout<<ans<<'\n';

}
signed main() {
    ios::sync_with_stdio(false), cin.tie(nullptr);

    int T; cin >> T;
    while (T--) {
        solve();
    }
}

G

背包 贪心

一个背包问题,新增一个条件:可以选不超过kkk个东西,免费获得,不占空间。

朴素的想法是增加一个维度表示使用了多少次免费次数,但这太慢了,完全不可行。可能这个数据,复杂度必须严格O(nW)O(nW)O(nW),多一点都会超。

那么dpdpdp优化就不太可行了,考虑贪心。一个比较自然的想法是,贪心用这个免费次数,要么用在价值高的上面,要么用在重量大的上面。

考虑优先用在重量大的上面,我们把www降序来考虑,但显然不是重量最大的几个直接用就完了,还得选。

这里有个重要的性质,如果wa>wbw_a>w_bwa>wb,不存在wbw_bwb免费选了,waw_awa花钱买的情况。因为1他们俩既然最后都要加入答案,那么交换一下,让重量大的免费,重量小的花钱,肯定不会更劣。那么一定存在一个分界点iii,前面的元素只存在免费买,后面的元素只存在花钱买。那么考虑枚举这个分界点,后面的元素花钱买,就是个朴素dpdpdp,预处理所有后缀的dpdpdp结果;前缀的元素可以至多选kkk个,那么选价值最大的kkk个,这就是维护前缀里的前kkk大,考虑堆维护topktopktopk

#include <bits/stdc++.h>
#define int long long
using namespace std;
constexpr int N = 5005, W = 10005;
int suf[N][W];
struct Item {
    int w, v;
    bool operator<(const Item &o) {
        return w > o.w;
    }
} item[N];
const int INF = 1e18;
void solve() {
    int n, W, k; cin >> n >> W >> k; 
    for (int i = 1; i <= n; i++) {
        cin >> item[i].w >> item[i].v;
    }
    suf[n+1][0] = 0;
    sort(item+1, item+1+n);
    for (int i = n; i >= 1; i--) {
        int wi = item[i].w, vi = item[i].v;
        for (int j = 0; j <= W; j++) {
            if (j >= wi)
                suf[i][j] = max(suf[i+1][j], suf[i+1][j-wi] + vi);
            else
                suf[i][j] = suf[i+1][j];
        }
    }
    priority_queue<int, vector<int>, greater<int>> pq;
    int sumv = 0, ans = suf[1][W];
    for (int i = 1; i <= n; i++) {
        int vi = item[i].v;
        pq.push(vi); sumv += vi;
        if ((int)pq.size() > k) {
            sumv -= pq.top();
            pq.pop();
        }
        ans = max(ans, sumv + suf[i+1][W]);
    }
    cout << ans << "\n";
}

signed main() {
    ios::sync_with_stdio(false), cin.tie(nullptr);

    int T = 1;
    while (T--) {
        solve();
    }
}

L

贪心 模拟

每层分别需要一定量的重量1/21/21/2的东西,电梯载重上限kkk,每次运行电梯的代价是到达的最高层数,每次运行电梯就是从地面装上一些东西,然后运行到某些层,卸货。问让每层的要求都满足的最小代价是多少?kkk是偶数

应该先从高层的需求开始运,因为给高层运的同时,可以额外给低层运,但反过来不行。那么一个朴素贪心就是每次给最高层运一个1/21/21/2,这样的问题是总需求量很大,直接模拟会超时。但总楼层数不多,这种时候就该考虑,一次处理完一层的需求,模拟的复杂度就能降低到O(n)O(n)O(n)级别。

每层的需求既有111也有222,讨论很麻烦。但注意到kkk是偶数,如果我们能把每层的需求的转化成只有222,这个贪心就好做很多了,只需要计算每层至少需要多少趟电梯,最后一趟可能存在空载,记录空载重量,处理下一层时,把下一层的需求减掉这部分空载(可以视为这部分被上一层的最后一次电梯运走了)。

把每一层的需求都转化成偶数,首先每层的222需求不用动,111需求如果是偶数也不用动。如果111需求是奇数,需要从更低层拿一个过来,可以视为把更底层的一个111也在这一层的最后一次电梯运了。优先拿也是奇数的更底层,这样一次可以解决两层的奇数,如果更低层都是偶数了,这次机会就浪费了,也就是这一层最后一班电梯会有111的空载。

转化完之后跑前面提到的贪心即可,每层计算电梯次数时都上取整,因为可能存在某一层需求是奇数,但根据我们上面的分析,多出来的111的空间就只能浪费了,我们把它上取整当成偶数。

void solve(){
	int n,k;
	cin>>n>>k;
	
	map<int,int>c1,c2;
	rep(i,1,n){
		int c,w,f;
		cin>>c>>w>>f;
		if(w==1){
			c1[f]+=c;
		}
		else{
			c2[f]+=c;
		}
	}	
	
	vvi t;
	for(auto &[k,v]:c1){
		t.push_back({k,v});
	}
	reverse(t.begin(),t.end());
	
	int m=t.size();
	for(int i=0,j=0;i<m;i++){
		if(t[i][1]%2){
			j=i+1;
			while(j<m&&t[j][1]%2==0){
				j++;
			}
			if(j<m){
				t[i][1]++;
				t[j][1]--;
			}
		}
	}
	
	for(auto &p:t){
		c2[p[0]]+=(p[1]+1)/2;
	}
	
	t.clear();
	for(auto &[k,v]:c2){
		t.push_back({k,v});
	}	
	
	reverse(t.begin(),t.end());
	int ans=0,space=0;
	for(auto &p:t){
		int f=p[0],cnt=p[1]*2;
		
		if(cnt>space){
			cnt-=space;
			space=0;
		}
		else{
			space-=cnt;
			cnt=0;
		}
		
		if(cnt){
//			cout<<f<<' '<<cnt<<'\n';
			ans+=(cnt+k-1)/k*f;
			int r=cnt%k;
			if(r){
				space+=k-r;
			}
		}
	}
	
	cout<<ans<<'\n';
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值