Android 13中的 Open Mobile API_sim trasmit apdu-程序员宅基地

技术标签: SE  android  移动安全  Android技术  

Open Mobile API Specification v3.2 简称 OMAPI

Android OMApi 接口架构和实现方式参考

Android OMApi 特性支持参考

aospxref Android 13 源码浏览网站

建议扩展阅读:

Android SE的多种形态: eSE、UICC、SD Card

Android 9.0 SecureElementService 初始化流程分析 很清晰的UML类图和调用流程

API 接口在 Specification 6.2 章节,本文基于 Android 13 的 OMAPI 实现(遵循 v3.3 规范),涉及了 framework 和 packages 层,具体实现与原始规范有些许不同


一、SE涉及场景及用途

SE 也就是 Secure Element,译为 “安全元素”

主要应用场景在 手机手表交通卡、门禁、虚拟钱包、虚拟SIM卡,以及其他身份认证的且对安全级别有一定要求的业务。

目前 Android 手机主要有三种SE的实现 eSEUICCmicroSD,这些带有SE的芯片有独立的储存和计算能力,可以进行 Applet 安装与个人化等一些列自定义的安全行为操作。

简单提一下这三种SE实现:

eSE:Embedded ,硬件内嵌在手机形式,通过 NFC Controller 使用
UICC:物理插槽形式,日常使用的SIM卡,在Android层面叫UICC,现在 eSIM 阶段新增了 EUICC,而且 Android 提供了一套另外的 eSIM 服务框架
microSD:物理插槽形式,早期使用的外部 SD 存储卡

Tips:
Android9 之前需引入 org.simalliance.openmobileapi.jar,从 Android9 开始并入了Android SDK,在 android.se.omapi 包下,且在 /packages/apps/ 下新增了SE相关模块(SecureElement


二、上层 framework

包路径:/frameworks/base/omapi/core/java/android/se/omapi/

目录下的所有文件:

Channel.java	
Reader.java
SEService.java
Session.java
ISecureElementChannel.aidl
ISecureElementListener.aidl	
ISecureElementReader.aidl
ISecureElementService.aidl
ISecureElementSession.aidl

Transport API class diagram overview

和原始的 OMAPI规范 还是有点区别
在这里插入图片描述


一、SEService.java

SEService(Context context, Executor executor, OnConnectedListener listener)

对应底层类 SecureElementService.java

内部接口: OnConnectedListener

初始化为耗时操作,异步进行,成功连接后进行回调

1.void onConnected();

方法:

- Reader getUiccReader(int)
- Reader[] getReaders() 返回可用的Reader列表,元素不能重复,即使没有插卡也返回,
	底层是 Terminal内部类SecureElementReader(Reader相关的通信也由它完成), 内部有 ArrayList<SecureElementSession> mSessions
- boolean isConnected() 当此服务成功连接时为true
- void shutdown() 释放所有由此SEService分配的SE资源,建议在程序结束时调用,此方法执行后,isConnected返回false
- String getVersion() 返回此实现基于哪一个 Open Mobile API 规范版本
	计算方法:版本号 = 主版本号*1000 + 副版本号,(例如:“3001”是基于OMAPI规范v3.1的实现)

二、Reader.java

Reader(@NonNull SEService service, @NonNull String name, @NonNull ISecureElementReader reader)

对应底层类 Terminal.java,此类非常重要(/packages/apps/SecureElement/src/com/android/se/Terminal.java)

方法:

- String getName() 
- SEService getSEService() 
- boolean reset()
- boolean isSecureElementPresent() 
- Session openSession() 
- void closeSessions() 

三、Session.java

Reader getReader() 
byte[] getATR() 
void close() 
boolean isClosed() 
void closeChannels() 
Channel openBasicChannel(byte[] aid) 
Channel openLogicalChannel(byte[] aid) 
Channel openBasicChannel(byte[] aid, Byte P2) 
Channel openLogicalChannel(byte[] aid, Byte P2)

四、Channel.java

void close() 
boolean isClosed()
boolean selectNext() 遍历匹配相同部分AID的所有applet, true: 在该通道上成功选择了一个新的Applet; false: 保持原有选中
boolean isBasicChannel()
Session getSession()
byte[] getSelectResponse()
byte[] transmit(byte[] command)
void setTransmitExpectDataWithWarningSW(boolean expectData)

三、framework流程:

通过 SEService 类构造传入注册服务接口,获取 Reader 得到 Session 实例,通过 Session 可以打开 Channel(包括 BasicChannelLogicalChannel),Channel 中提供了发送 APDU 命令的方法。

总结步骤:

  1. 连接 SEService,需要几百毫秒时间
  2. 指定 Reader 或者 getReaders 遍历取出能使用的 Reader
  3. 通过 Reader 连接一个 Session
  4. 通过 Session 的 openBasicChannel(aid, p2) / openLogicChannel(aid, p2) 打开一个 Channel
  5. 通过 Channel.transmit(cmd) 发送 APDU 指令
  6. 通过 Channel.close() 关闭通道
  7. 通过 Session 关闭会话,或者通过 Reader.closeSessions() 关闭所有已打开的会话
  8. 通过 SEService.shutdown() 断开自身连接,此方法内包了含上一步骤

四、底层 packages

包路径:/packages/apps/SecureElement/src/com/android/se/

目录下的文夹件和类:

internal/
security/
Channel.java	
CommandApduValidator.java	
SEApplication.java	
SecureElementService.java	
Terminal.java

一、SecureElementService.java

getReader()
createTerminals() {
    addTerminals(ESE_TERMINAL);
    addTerminals(UICC_TERMINAL);
}
addTerminals(String terminalName) {
    ...
    name = terminalName + Integer.toString(index);
    Terminal terminal = new Terminal(name, this);
    terminal.initialize(index == 1);
    mTerminals.put(name, terminal);
    ...
}

// Seesion 的Binder桥接
final class SecureElementSession extends ISecureElementSession.Stub {
    closeChannels()
    openBasicChannel(...)
    openLogicalChannel(...)
}

二、Terminal.java

setUpChannelAccess(...)
isSecureElementPresent()
initialize(boolean retryOnFail)
initializeAccessControl() {
    if (mAccessControlEnforcer == null) {
        mAccessControlEnforcer = new AccessControlEnforcer(this);
    }
    mAccessControlEnforcer.initialize();
}

byte[] transmit(byte[] cmd);
byte[] transmitInternal(byte[] cmd) {
	...
	// mSEHal 也就是 SecureElement.java
	// 年轻人我劝你不要去看后面的源码了, 深得很你把握不住, 硬件层的各种 CPP
	mSEHal.transmit(byteArrayToArrayList(cmd));
}

// Reader 的Binder桥接
final class SecureElementReader extends ISecureElementReader.Stub {
	getAtr()
	reset()
	openSeesion()
	isSecureElementPresent()
	private getTerminal()
}

三、AccessControlEnforcer.java

initialize()
reset()
setUpChannelAccess(...)
readSecurityProfile()
arf...PKCS15
ara...

四、Channel.java

byte[] transmit(byte[] command) {
	CommandApduValidator.execute(command);
	checkCommand(command);

	// 一大堆命令检查, 接着还是通过 Terminal 发送
	return mTerminal.transmit(command);
}

final class SecureElementChannel extends ISecureElementChannel.Stub {
	close()
	boolean selectNext()
	byte[] transmit(byte[] command) {
		Channel.this.transmit(command);
	}
}

常用名词

Trusted Service Manager (TSM)
Issuer Security Domain (ISD) 
Access Rule Files (ARF)
Access Rule Application (ARA)
Access Rule Application Master (ARA-M)
Access Rule Application Client (ARA-C)

五、packages流程

framework 层的 SEService.java 在构造里通过 ISecureElementChannel.aidl 类绑定到了
packages 层的服务 SecureElementService.java 上,后续都通过此服务交互。以下三个比较重要的入口类,SecureElementService 会在 onCreate() 里初始化,并创建多个 Terminal 实例,主要是 eSEUICC(SIM),它们可能会有多个,类似(eSE1、eSE2、SIM1、SIM2)。

这里即是上层调用的 openSeesionopenBasicChannel()openLogicalChannel()C++ 层的入口,也就是在这里做了 AccessRule 的校验(我在这儿就遇到了问题),规则校验具体实现入口在 AccessControlEnforcer.java,此类在 Terminal 中初始化。

顺便提两句,第一个是在上层调用 openSeesion 过程中有一个 isSecureElementPresent() 的检查,如果返回 false 会直接抛出异常 “Secure Element is not present.”,也就是当前 SE 不可用(具体原因暂时未解,查看了一些cpp的实现里直接返回的true,但上层确实有返回false,排除异常的情况),所以调用前需要在 Reader 里先行判断。

第二个是 openXXXChannel() 过程中,会有一个 setUpChannelAccess() 方法进行访问规则的校验,关于 AccessRule GPD 有一本非常厚的 SE_Access_Control_v1.1.pdf 的规范描述。

当规则校验通过后,Channel 也就打开了,此时可以在上层使用 Channel.transmit() 发送 APDU 指令,且此方法进行了响应。可以是一条指令也可以是多条,发送完成后即返回结果,另外还提供了一个方法用来获取结果 getSelectResponse(),没太细究它们俩直接的区别。

五、使用案例

5.1 检查设备支持

可以使用 PackageManager.hasSystemFeature 检查设备是否支持需要的 SE 区域,或者使用 PackageManager.systemAvailableFeatures 列出所有支持的特性,从里面找如下三个

FEATURE_SE_OMAPI_ESE
FEATURE_SE_OMAPI_UICC
FEATURE_SE_OMAPI_SD

Android 官方相关描述:

Open Mobile API reader support

On Android 11 and higher, Open Mobile API (OMAPI) supports checking for eSE, SD, and UICC support hardware on devices with the following flags:

FEATURE_SE_OMAPI_ESE
FEATURE_SE_OMAPI_SD
FEATURE_SE_OMAPI_UICC

Use these values with getSystemAvailableFeatures() or hasSystemFeature() to check for device support.



	if (requireContext().packageManager.hasSystemFeature(PackageManager.FEATURE_SE_OMAPI_UICC)) {
        SIMLog.e("系统是否支持 OMAPI UICC_SE 硬件功能:true")
        lifecycleScope.launch {
           delay(2000)
           mOperator.getEID() // 做了个测试调用, 里面使用 OMAPI 执行了一次 APDU 发送
        }
    } else {
        SIMLog.e("系统是否支持 OMAPI UICC_SE 硬件功能:false")
    }

    requireContext().packageManager.systemAvailableFeatures.forEach {
        Log.e("Flyme-SIM", "系统已支持的硬件功能:$it")
    }

5.2 客户端调用 SEService.java

使用了 framework 下的类 SEService.java,也就是SDK自带的包 android.se.omapi,调用的相关日志在后续有贴出来,主要是经历了一个错误记录下来


	// Android 客户端代码

	import android.se.omapi.SEService 

 	private var mUICCReader: Reader? = null
    private val mSEService = SEService(context, ThreadUtils.getSinglePool()) {
    
        SIMLog.e("SEService已连接", TAG)
    }

    init {
    
		// SEService 大概需要几百毫秒进行连接
        ThreadUtils.getMainHandler().postDelayed({
    
            SIMLog.e("SEService连接状态: ${
      mSEService.isConnected}", TAG)

            if (mSEService.isConnected) {
    
	//             mUICCReader = mSEService.getUiccReader(SLOT_INDEX) // 指定卡槽拿到的Reader不是SE, 直接使用遍历的方式
				// 我的遍历结果
				// 已存在的Reader: name=eSE1, isSecureElementPresent=true
				// 已存在的Reader: name=SIM1, isSecureElementPresent=false
				// 已存在的Reader: name=SIM2, isSecureElementPresent=false
                for (reader in mSEService.readers) {
    
                    SIMLog.printD("已存在的Reader: name=${
      reader.name}, isSecureElementPresent=${
      reader.isSecureElementPresent}")
                    if (reader.isSecureElementPresent) mUICCReader = reader
                }
            }
        }, 1000)
    }


    /**
     * 发送 APDU 指令
     * 一次发送, 包含 选择Applet、打开通道、读取响应、关闭通道
     * 1.打开 Reader、Session、Channel
     * 2.通过 Channel 选择 Applet,并打开逻辑通道
     * 3.发送 APDU,并接收响应
     * 4.关闭 Reader、Session、Channel
     *
     * @param p2 '00', '04', '08', '0C'
     */
    private fun send(command: ByteArray, aid: ByteArray = AID_BYTE, p2: Byte = 0x00): ByteArray? {
    
        var resp: ByteArray? = null

        mUICCReader?.let {
     reader ->
            log("sendApdu: Reader.isSecureElementPresent=${
      reader.isSecureElementPresent}")

            try {
    
                if (!reader.isSecureElementPresent) {
    
                    log("此 Reader 不支持 SE")
                    return null
                }
                val openSession = reader.openSession()

				// 在执行 openSession.openLogicalChannel(aid, p2) 时,遇到一个错误
                // java.lang.SecurityException:
                // Exception in setUpChannelAccess() java.security.AccessControlException: SecureElement-AccessControlEnforcerno APDU access allowed!
                openSession.openLogicalChannel(aid, p2)?.let {
    
                    log("发送的req: ${
      command.bytesToHexString()}")
                    it.transmit(command)

                    resp = it.selectResponse
                    log("返回的resp: ${
      resp?.bytesToHexString()}")
                    log("返回是否成功: ${
      isSendSuccess(resp)}")

                    it.close()
                }
                openSession.close()
            } catch (e: Exception) {
    
                log("发送APDU异常: $e")
            }
        } ?: kotlin.run {
    
            log("mUICCReader还未初始化完成, SEService还在连接中")
        }

        return resp
    }


5.3 主要讲一下这个错误

执行 openSession.openLogicalChannel(aid, p2) 遇到错误:

java.lang.SecurityException:
Exception in setUpChannelAccess()
java.security.AccessControlException:SecureElement-AccessControlEnforcerno APDU access allowed!

这个问题出现是正常的,因为OMAPI的规范里是有AC规则校验的,正常情况下应用什么都没做去访问SE肯定是没有权限的。
解决方法1:如果是使用SIM-SE,就把应用hash写入SIM卡,如果是使用eSE,就把应用hash写入手机系统;
解决方法2:修改系统源码,注释掉AC规则校验的相关逻辑,我尝试了下面的方式修改校验,结果是无效的

5.3.1

查看源码:

/frameworks/base/omapi/java/android/se/omapi/Session.java

	
	Session.java

    public @Nullable Channel openLogicalChannel(@Nullable byte[] aid, @Nullable byte p2) throws IOException {
    
        if (!mService.isConnected()) {
    
            throw new IllegalStateException("service not connected to system");
        }
        synchronized (mLock) {
    
            try {
    
				// 执行到这一行, 调用到 ISecureElementChannel.aidl
                ISecureElementChannel channel = mSession.openLogicalChannel(
                        aid,
                        p2,
                        mReader.getSEService().getListener());
                if (channel == null) {
    
                    return null;
                }
                return new Channel(mService, this, channel);
            } catch (ServiceSpecificException e) {
    
               ...
            } catch (RemoteException e) {
    
                throw new IllegalStateException(e.getMessage());
            }
        }
    }

5.3.2

后续调用离开了 framework 层到了底层目录 packages:

错误里提到一个关键类和一个方法:AccessControlEnforcerno.javasetUpChannelAccess(),该类在 Terminal.java 中初始化

首先来看下 Terminal.java


	Terminal.java

    /**
    * Initializes the Access Control for this Terminal
    */
    private synchronized void initializeAccessControl() throws IOException,
           MissingResourceException
    {
    
		...

        synchronized(mLock) {
    
            if (mAccessControlEnforcer == null) {
    
                mAccessControlEnforcer = new AccessControlEnforcer (this);
            }

            try {
    
                mAccessControlEnforcer.initialize();
            } catch (IOException | MissingResourceException e) {
    
                mAccessControlEnforcer = null;
                throw e;
            }
        }
    }

	/**
	 * Opens a logical Channel with AID for the given package name or uuid
	 */
	public Channel openLogicalChannel(SecureElementSession session, byte[] aid, byte p2,
		ISecureElementListener listener, String packageName,
		byte[] uuid, int pid) throws IOException, NoSuchElementException {
    

		...

		ChannelAccess channelAccess = null;
		if (packageName != null || uuid != null) {
    
			channelAccess = setUpChannelAccess(aid, packageName, uuid, pid, false);
		}

		...
		return logicalChannel;
	}


	// 在 Terminal.java 同名的 setUpChannelAccess 方法里调用了 mAccessControlEnforcer.setUpChannelAccess
	/**
	 * Initialize the Access Control and set up the channel access.
	 */ 
	private ChannelAccess setUpChannelAccess(byte[] aid, String packageName, byte[] uuid, int pid,
              boolean isBasicChannel) throws IOException, MissingResourceException {
    

		ChannelAccess channelAccess =
                          mAccessControlEnforcer.setUpChannelAccess(aid, packageName, uuid, checkRefreshTag);
	}

5.3.3

接下来就到了最终目的类 AccessControlEnforcerno.java

/packages/apps/SecureElement/src/com/android/se/security/AccessControlEnforcerno.java


	AccessControlEnforcerno.java

	/** Initializes the Access Control for the Secure Element */
    public synchronized void initialize() throws IOException, MissingResourceException {
    
		...
		readSecurityProfile();
		...
	}

	// 后面两个方法就是完整代码了

	/** Sets up the Channel Access for the given Package */
	public ChannelAccess setUpChannelAccess(byte[] aid, String packageName, byte[] uuid,
        boolean checkRefreshTag) throws IOException, MissingResourceException {
    
        ChannelAccess channelAccess = null;
        // check result of channel access during initialization procedure
        if (mInitialChannelAccess.getAccess() == ChannelAccess.ACCESS.DENIED) {
    
                throw new AccessControlException(mTag + "access denied: " + mInitialChannelAccess.getReason());
            }

        // this is the new GP Access Control Enforcer implementation
        if (mUseAra || mUseArf) {
    
           channelAccess = internal_setUpChannelAccess(aid, packageName, uuid,checkRefreshTag);
        }

        if (channelAccess == null || (channelAccess.getApduAccess() != ChannelAccess.ACCESS.ALLOWED
                    && !channelAccess.isUseApduFilter())) {
    

				// 关键点来了, 摆明了就是一个系统权限问题
                if (mFullAccess) {
    
                    // if full access is set then we reuse the initial channel access,
                    // since we got so far it allows everything with a descriptive reason.
                    channelAccess = mInitialChannelAccess;
                } else {
    
					// 错误就是在这里抛出的, mFullAccess 是个全局方法, 在初始化时调用了 
                    throw new AccessControlException(mTag + "no APDU access allowed!");
                }
            }
        channelAccess.setPackageName(packageName);
        return channelAccess.clone();
    }

    private void readSecurityProfile() {
    
		// 非 debug 模式下写死了 mFullAccess = false 的,可以将手机 root 或者使用 magisk app 修改为 Debug
        if (!Build.IS_DEBUGGABLE) {
    
             mUseArf = true;
             mUseAra = true;
             mFullAccess = false; // Per default we don't grant full access.
         } else {
    
             String level = SystemProperties.get("service.seek", "useara usearf");
             level = SystemProperties.get("persist.service.seek", level);
    
             if (level.contains("usearf")) {
    
                mUseArf = true;
             } else {
    
                mUseArf = false;
             }
             if (level.contains("useara")) {
    
                mUseAra = true;
             } else {
    
                mUseAra = false;
             }
             if (level.contains("fullaccess")) {
    
				// 全局只有这一处将 mFullAccess 赋值成了 true
				// 也就是上面 SystemProperties 配置的 "service.seek" 值起了决定性作用
                mFullAccess = true;
             } else {
    
                mFullAccess = false;
             } 
         }
        
        if (!mTerminal.getName().startsWith(SecureElementService.UICC_TERMINAL)) {
    
              // ARF is supported only on UICC.
              mUseArf = false;
        }
        Log.i(mTag, "Allowed ACE mode: ara=" + mUseAra + " arf=" + mUseArf + " fullaccess="+ mFullAccess);
    }

5.3.4 修改系统属性

在系统已经 root 的情况下,直接使用命令修改掉这两个字段的值

adb shell setprop ro.debuggable "1" //正常情况下 已root 设备就是 1
adb shell setprop service.seek "useara usearf fullaccess"
adb shell setprop persist.service.seek "useara usearf fullaccess"

#设置后也可以查看
adb shell getprop service.seek

5.3.5 调用日志

17:56:39.838 4481  SecureElementService                com.android.se    D  getReaders() eSE1
17:56:39.838 4481  SecureElementService                com.android.se    D  getReaders() SIM1
17:56:39.838 4481  SecureElementService                com.android.se    D  getReaders() SIM2
17:56:39.838 4481  SecureElementService                com.android.se    I  isCtsRunning false
17:56:39.838 4481  SecureElementService                com.android.se    D  getReader() SIM1
17:56:39.839 4481  SecureElementService                com.android.se    D  getReader() SIM2
17:56:39.839 4481  SecureElementService                com.android.se    D  getReader() eSE1

17:56:40.856 4481  SecureElementService                com.android.se    I  openLogicalChannel() AID = 00a4040000, P2 = 0
17:56:40.857 4481  SecureElement-Terminal-eSE1         com.android.se    I  mzoma Access Check, pkg is com.ccsmec.sim
17:56:40.859 4481  SecureElement-Terminal-eSE1         com.android.se    W  Enable access control on logical channel for com.ccsmec.sim
17:56:40.859 4481  SecureElement-AccessControlEnforcer com.android.se    I  setUpChannelAccess() aid = 00a4040000
17:56:40.859 4481  SecureElement-AccessControlEnforcer com.android.se    I  setUpChannelAccess() packageName = com.ccsmec.sim
17:56:40.860 4481  SecureElement-Terminal-eSE1         com.android.se    I  mzoma Access Check, pkg is com.ccsmec.sim
17:56:40.915 4481  SecureElement-AccessControlEnforcer com.android.se    I  checkCommand() : Access = ALLOWED APDU Access = ALLOWED Reason = Unspecified
17:56:40.928 4481  SecureElement-Terminal-eSE1         com.android.se    I  Sent : 81cadf2000
17:56:40.928 4481  SecureElement-Terminal-eSE1         com.android.se    I  Received : df20084a46485fc8d7b30b9000
17:56:40.928 4481  SecureElement-AraController         com.android.se    I  Refresh tag unchanged. Using access rules from cache.
17:56:40.962 4481  SecureElement-AccessControlEnforcer com.android.se    I  getAccessRule() appCert = dba6d7c5929b57a81387d144da3d04a5a3f32137
17:56:40.962 4481  SecureElement-AccessControlEnforcer com.android.se    I  getAccessRule() appCert = a89c2be6dbe8eefbcce61b5c02a04d99862ef42fbb147c1a7f1d14e742e7db54
17:56:40.964 4481  SecureElement-AccessRuleCache       com.android.se    I  findAccessRule() not found
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/qq_39420519/article/details/127904009

智能推荐

CamVid数据集(智能驾驶场景的语义分割)_camvid数据集11类别-程序员宅基地

文章浏览阅读3.3k次,点赞2次,收藏7次。前言CamVid 数据集是由剑桥大学公开发布的城市道路场景的数据集。CamVid全称:The Cambridge-driving Labeled Video Database,它是第一个具有目标类别语义标签的视频集合。数据集包 括 700 多张精准标注的图片用于强监督学习,可分为训练集、验证集、测试集。同时, 在 CamVid 数据集中通常使用 11 种常用的类别来进行分割精度的评估,分别为:道路 (Road)、交通标志(Symbol)、汽车(Car)、天空(Sky)、行人道(Sidewalk)、电_camvid数据集11类别

ESP32 LVGL8.1 实现太空人显示(29)_lv_img_declare-程序员宅基地

文章浏览阅读8.7k次,点赞11次,收藏90次。文章目录一、ESP32 LVGL工程配置1.1从库中下载LVGL代码1.2配置适合ESP32 液晶屏1.3编译下载测试二、GIF图片处理2.1下载gif图片2.2将gif图片按照帧率导出成图片2.lvgl animimg对象实现图片的播放1.3下载测试 注:本博客作为学习笔记,有错误的地方希望指正一、ESP32 LVGL工程配置首先要通过液晶屏显示太空人,我们这里主要有两种方式可以实现,第一种直接使用厂家只带的液晶屏幕驱动去实现图片的显示,另外使用其他的GUI提供的控件去实现,嵌入式常见的GUI挺多_lv_img_declare

最小权顶点覆盖问题-程序员宅基地

文章浏览阅读1.5w次,点赞7次,收藏30次。问题描述:给定一个赋权无向图G=(V,E),每个顶点v∈V都有一个权值w(v)。如果UV,且对任意(u,v)∈E有u∈U或v∈U,就称U为图G的一个顶点覆盖。G的最小权顶点覆盖是指G中所含顶点权之和最小的顶点覆盖。问题解决:用优先队列分支限界方法解最小权顶点覆盖,在算法的搜索的进程中保存当前已构造出的部分解空间树,在算法搜索达到叶节点时,其最优值对应的最优解同时保存下来。优先队列的优先_最小权顶点覆盖

matlab如何导入map,matlab添加M_map工具箱-程序员宅基地

文章浏览阅读517次。首先试了matlab自带的worldmap,感觉画出来的图形不尽如人意,比较杂乱。如下图。查阅了些资料,请教了Liangjing,一致推荐m_map。为了达到想要的效果,这次只要不再偷懒,下载M-Map工具箱(http://www.eos.ubc.ca/~rich/map.html)并进行安装。所幸过程比较顺利,现记录如下,回头把画出的效果图再添上。其他matlab的toolbox安装,也可参考进..._mmap工具包如何安装

iOS学习:iOS代码规范_ios replaceobjectsinrange-程序员宅基地

文章浏览阅读5.7k次,点赞4次,收藏5次。作者感言阅读前言iOS代码规范Import规范Define规范Paragma Mark 规范Interface规范implementation规范实例规范NSDictionary规范NSArray规范函数规范If-Else规范For-In For 规范Block规范运算符规范命名规范实例命名规范Property命名规范Interface-class命名规范B_ios replaceobjectsinrange

python 将列表中的字符串转为数字格式,以及pandas Series或index转换为Numpy数组_pandas index存入数组-程序员宅基地

文章浏览阅读1.7w次,点赞7次,收藏39次。本文实例讲述了Python中列表元素转为数字的方法分享给大家供大家参考,具体如下:1.首先可以用循环来解决(直接粗暴)举个栗子:<1>有一个数字字符的列表:numbers = ['2', '5', '130', '8']`<2>想要把每个元素转换为数字:numbers = [2, 5, 130, 8]用一个循环来解决:new_numbers = [];..._pandas index存入数组

随便推点

maven 依赖文件 pom.xml 编译 mvn compile 运行 不用mvn exec:java -Dexec.mainClass="hello.HelloWorld"...-程序员宅基地

文章浏览阅读289次。使用maven编译Java项目http://blog.csdn.net/yaya1943/article/details/48464371使用"mvn clean"命令清除编译结果,也就是把编译生成的target文件夹删掉如果你想安装您的项目的JAR文件到本地Maven仓库,那么你应该调用下面语句:mvn install此时,你的项目代码将会经过编译、测试、打..._maven 编译不使用本地依赖包

巧用ChatGPT快速提高职场晋升力、搞定数据分析玩、转新媒体运营-程序员宅基地

文章浏览阅读7.6k次,点赞147次,收藏134次。在日常工作中巧用ChatGPT可以帮助我们提高工作效率、创造价值并降低成本。通过合理地利用ChatGPT的功能和应用场景,企业和个人可以更好地实现工作目标、提升竞争力并取得更大的成功。随着人工智能技术的不断进步和发展我们相信巧用ChatGPT将成为未来工作中的一种常态化工具为我们的职业生涯和生活带来更多便利和价值。本书是一本关于数据分析与ChatGPT应用的实用指南,旨在帮助读者了解数据分析的基础知识及利用ChatGPT进行高效的数据处理和分析。_巧用chatgpt快速提高职场晋升力

vscode同步git代码时源代码管理出现10k+更改如何处理?_git init 代码有 10k+ 更新-程序员宅基地

文章浏览阅读2.7k次。vscode出现10k+更改需要处理_git init 代码有 10k+ 更新

机器学习算法之K-means(K均值聚类)算法_k-means聚类算法的anchors_num的最大值-程序员宅基地

文章浏览阅读6.1k次,点赞5次,收藏124次。聚类聚类,简单来说,就是将一个庞杂数据集中具有相似特征的数据自动归类到一起,称为一个簇,簇内的对象越相似,聚类的效果越好。它是一种无监督的学习(Unsupervised Learning)方法,不需要预先标注好的训练集。聚类与分类最大的区别就是分类的目标事先已知,例如猫狗识别,你在分类之前已经预先知道要将它分为猫、狗两个种类;而在你聚类之前,你对你的目标是未知的,同样以动物为例,对于一个动物集来..._k-means聚类算法的anchors_num的最大值

edu教育邮箱免费申请注册Google drive无限网盘和微软OneDrive经验分享_onedrive a5-程序员宅基地

文章浏览阅读1.6w次,点赞2次,收藏15次。前言介绍很多萌新小白初涉国外云盘对Google云盘和微软OneDrive云盘到底容量有多大、数据隐私是否安全、自己免费申请白嫖的账户可用多久都没有清晰准确的认识和了解,又加上网上博客文章大多都是很多年前的方法和政策,完全给不了网友实质上的建议,本文来帮大家屡屡头绪。产品认识Google DriveGoogle Drive云端硬盘提供了15G 的免费容量,对于一般文档是够用了。Google drive唯一的好处就说分享资源超级方便,共享也很容易。Google 照片提供不限容量的照片存储_onedrive a5

把平板、手机作为电脑第二屏幕(Linux系统下)_平板副屏linux-程序员宅基地

文章浏览阅读1.5w次,点赞18次,收藏69次。将平板、手机作为电脑第二屏幕(Linux系统下)背景把手机、平板作为电脑第二屏幕是上个学期偶然想到的,那时我一边看网上的教程一边码代码。由于看的是视频教程,缩小了就看不清上面的字,放大了又会挡住打字区域。这时就想如果有个第二屏幕就很方便了,买个屏幕又不太方便,想到自己还有个平板天天吃灰,这不是就是现成的屏幕嘛。在网上搜了搜发现真的有解决方案,windows和mac上有很多软件可以用,但我平时..._平板副屏linux

推荐文章

热门文章

相关标签